岩本隆史の日記帳(アーカイブ)

はてなダイアリーのサービス終了をうけて移行したものです。更新はしません。

HUnitを使ってみた(ついでにHLintも)

HaskellユニットテストフレームワークであるHUnitを使ってみました。テスト対象のコードは、先日書いたsign関数です。

HUnitのインストール

なにはともあれ、まずはHUnitをインストールしました。Cabalのおかげで簡単に入れられました。

$ cabal install HUnit

テスト対象コードのモジュール名検討

つづいて、テスト対象コードのモジュール名を何にするか検討しました。このモジュールは「キープリスト」というWebアプリケーションで使う予定であり、AmazonAPIに関わるものなので、「KeepList.Amazon」に決めました。

ファイル構成の検討

つづいて、テスト対象コードとテストコードをどのように配置するか検討しました。モジュール名の名前空間の階層をそのまま生かし、テストコードを「tests」ディレクトリに入れることに決めました。

$ tree
.
|-- KeepList
|   `-- Amazon.hs
`-- tests
    `-- KeepList
        `-- Amazon.hs

テスト対象コードの記述

つづいて、テスト対象コード(KeepList/Amazon.hs)を記述しました。といっても、先日書いたコードのモジュール名を「KeepList.Amazon」に変え、main関数を削除し、sign関数の名前を「signature」に変えただけです。

module KeepList.Amazon (signature) where

import Data.ByteString.Lazy.Char8 (pack)
import Data.Digest.Pure.SHA (hmacSha256, bytestringDigest)
import Data.ByteString.Lazy (unpack)
import Codec.Binary.Base64 (encode)

signature :: String -> String -> String
signature secret_key message = encode $ unpack $ bytestringDigest $ hmacSha256 (pack secret_key) (pack message)

テストコードの記述

つづいて、テストコード(tests/KeepList/Amazon.hs)を記述しました。署名の例はAmazon Product Advertising APIのドキュメントから集めました。テストの書き方は「第16回 Haskellでのテストの自動化を考える(2ページ目) | 日経 xTECH(クロステック)」を参考にしました。

module Main where

import Test.HUnit
import KeepList.Amazon

secret_key = "1234567890"

tests = test
  [ "TuM6E5L9u/uNqOX09ET03BXVmHLVFfJIna5cxXuHxiU="
    ~=? signature secret_key ("GET\necs.amazonaws.co.uk\n/onca/xml\nA" ++
                              "WSAccessKeyId=00000000000000000000&Act" ++
                              "or=Johnny%20Depp&AssociateTag=mytag-20" ++
                              "&Operation=ItemSearch&ResponseGroup=It" ++
                              "emAttributes%2COffers%2CImages%2CRevie" ++
                              "ws%2CVariations&SearchIndex=DVD&Servic" ++
                              "e=AWSECommerceService&Sort=salesrank&T" ++
                              "imestamp=2009-01-01T12%3A00%3A00Z&Vers" ++
                              "ion=2009-01-01"),

    "cF3UtjbJb1+xDh387C/EmS1BCtS/Z01taykBCGemvUU="
    ~=? signature secret_key ("GET\necs.amazonaws.com\n/onca/xml\nAWS" ++
                              "AccessKeyId=00000000000000000000&Assoc" ++
                              "iateTag=mytag-20&Item.1.OfferListingId" ++
                              "=j8ejq9wxDfSYWf2OCp6XQGDsVrWhl08GSQ9m5" ++
                              "j%2Be8MS449BN1XGUC3DfU5Zw4nt%2FFBt87cs" ++
                              "pLow1QXzfvZpvzg%3D%3D&Item.1.Quantity=" ++
                              "3&Operation=CartCreate&Service=AWSECom" ++
                              "merceService&Timestamp=2009-01-01T12%3" ++
                              "A00%3A00Z&Version=2009-01-01"),

    "aMFgBNKPrz9PRR9Ato7yanlaG/PkQsNxIWYbLD1V9Zc="
    ~=? signature secret_key ("GET\necs.amazonaws.jp\n/onca/xml\nAWSA" ++
                              "ccessKeyId=00000000000000000000&Associ" ++
                              "ateTag=mytag-20&ListType=WishList&Name" ++
                              "=wu&Operation=ListSearch&Service=AWSEC" ++
                              "ommerceService&Timestamp=2009-01-01T12" ++
                              "%3A00%3A00Z&Version=2009-01-01"),

    "H5u4W10g0vmyB1KA6hmkrea36AFvSryL9SQfPejvWNs="
    ~=? signature secret_key ("GET\necs.amazonaws.com\n/onca/xml\nAWS" ++
                              "AccessKeyId=00000000000000000000&Assoc" ++
                              "iateTag=mytag-20&ListId=34AN6HPUN5AMX&" ++
                              "ListType=WishList&Operation=ListLookup" ++
                              "&ResponseGroup=ListItems%2COffers%2CIm" ++
                              "ages&Service=AWSECommerceService&Times" ++
                              "tamp=2009-01-01T12%3A00%3A00Z&Version=" ++
                              "2009-01-01"),

    "kEXxAIqhh6eBhLhrVMz2gt3ocMaH/OBVPbjvc9TG8ao="
    ~=? signature secret_key ("GET\necs.amazonaws.com\n/onca/xml\nAWS" ++
                              "AccessKeyId=00000000000000000000&Assoc" ++
                              "iateTag=mytag-20&BrowseNodeId=465600&O" ++
                              "peration=BrowseNodeLookup&ResponseGrou" ++
                              "p=BrowseNodeInfo%2CTopSellers%2CNewRel" ++
                              "eases%2CMostWishedFor%2CMostGifted&Ser" ++
                              "vice=AWSECommerceService&Timestamp=200" ++
                              "9-01-01T12%3A00%3A00Z&Version=2009-01-" ++
                              "01"),

    "I2pbqxuS/mZK6Apwz0oLBxJn2wDL5n4kFQhgYWgLM7I="
    ~=? signature secret_key ("GET\necs.amazonaws.com\n/onca/xml\nAWS" ++
                              "AccessKeyId=00000000000000000000&Assoc" ++
                              "iateTag=mytag-20&Condition=New&ItemId=" ++
                              "B0011ZK6PC%2CB000NK8EWI&Merchant=Amazo" ++
                              "n&Operation=SimilarityLookup&ResponseG" ++
                              "roup=Offers%2CItemAttributes&Service=A" ++
                              "WSECommerceService&SimilarityType=Inte" ++
                              "rsection&Timestamp=2009-01-01T12%3A00%" ++
                              "3A00Z&Version=2009-01-01") ]

テストの実行

以上で、テストが実行できるようになりました。実行すると、下記の通り、6ケースとも成功しました。

$ ghci
GHCi, version 6.12.2: http://www.haskell.org/ghc/  :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Loading package ffi-1.0 ... linking ... done.
Prelude> :load tests/KeepList/Amazon.hs
[1 of 2] Compiling KeepList.Amazon  ( KeepList/Amazon.hs, interpreted )
[2 of 2] Compiling Main             ( tests/KeepList/Amazon.hs, interpreted )
Ok, modules loaded: Main, KeepList.Amazon.
*Main> runTestTT tests
Loading package HUnit-1.2.2.1 ... linking ... done.
Loading package array-0.3.0.0 ... linking ... done.
Loading package syb-0.1.0.2 ... linking ... done.
Loading package base-3.0.3.2 ... linking ... done.
Loading package bytestring-0.9.1.6 ... linking ... done.
Loading package containers-0.3.0.0 ... linking ... done.
Loading package binary-0.5.0.2 ... linking ... done.
Loading package SHA-1.4.1.1 ... linking ... done.
Loading package dataenc-0.13.0.2 ... linking ... done.
Cases: 6  Tried: 6  Errors: 0  Failures: 0
Counts {cases = 6, tried = 6, errors = 0, failures = 0}
*Main> :q
Leaving GHCi.

HLintによるコードチェック

テストは通ったのですが、いまひとつコードの書き方に自信がないので、「How to write a Haskell program - HaskellWiki」で知ったHLintでコードをチェックすることにしました。

HLintのインストールは下記の手順でできました。

$ cabal install happy
$ cabal install haskell-src-exts
$ cabal install hlint

コードをチェックした結果が下記のものです。

$ hlint KeepList/Amazon.hs
No suggestions
$ hlint tests/KeepList/Amazon.hs
tests/KeepList/Amazon.hs:6:1: Warning: Use camelCase
Found:
  secret_key = ...
Why not:
  secretKey = ...

1 suggestion

テスト対象コードは問題なしでしたが、テストコードには一つ問題が見つかりました。『変数名の「secret_key」だけどさ、キャメルケースの「secretKey」にしたら?』ということですね。

コードは割愛しますが、テストコードを修正し、無事「No suggestions」となりました。じつにありがたいツールです。

今後の展望

今回はテスト対象コードが一つだけだったので、HUnitの「runTestTT」でテストを走らせましたが、数が増えてきたら、この方法では手間がかかって仕方ありません。

第16回 Haskellでのテストの自動化を考える(4ページ目) | 日経 xTECH(クロステック)」で紹介されているような、Cabalによる複数テスト実行が必要になると思っています。