このエントリは 前回 に引き続き、本日(2017-9-7)発売の Haskell入門 へページ数の都合で載せられなかった記事に、加筆修正をして公開するものです。このエントリの内容は、10.6節の後に載せる予定だったものです。
今回テスト対象とするアプリケーションは、公開されている Haskell入門のサンプルコード を参照して下さい。 Haskell入門 の10章では、このアプリケーションの作り方を解説しています。
単体テスト
WAIにはWAIアプリケーションをテストするためのNetwork.Wai.Testというモジュールが用意されています。本節ではNetwork.Wai.Testの簡単な使い方を紹介するために、Weight Recorderアプリケーションの単体テストを作成します。未登録のユーザがWeight Recorderアプリケーションへアクセスし、ユーザ登録、ログイン、体重の登録といった操作をすべて呼び出せるかを確認します。
単体テストの実装
test/test.hs へ単体テストを記述します。ここでは、テストの実行はシンプルにTest.HUnitモジュールのrunTestTTを使うことにしましょう。
import 文の一覧は次の通りです。
{-# LANGUAGE OverloadedStrings #-}
module Main (main) where
import Control.Monad (void)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS
import qualified Data.ByteString.Lazy.UTF8 as LBS
import qualified Network.HTTP.Types as HTTP
import qualified Network.Wai as WAI
import qualified Network.Wai.Test as WT
import Paths_weight_recorder (getDataDir)
import System.FilePath ((</>))
import System.IO.Temp (withSystemTempFile)
import System.Process (callCommand)
import Test.HUnit
( Assertion
, Test (TestCase)
, runTestTT
)
import Web.Spock (spockAsApp)
import Web.WeightRecorder
( WRConfig (WRConfig, wrcDBPath, wrcPort, wrcTplRoots)
, weightRecorderMiddleware
)
testWeightRecorderはAssertion型で、これが実行されるテストです。Weight Recorderアプリケーションの実行にはSQLiteのデータベースが必要なので、まずはそれを作ります。withSystemTempFile関数によって空の一時ファイルを作成し、callCommand I/Oアクションでsqlite3 コマンドにスキーマ定義ファイルを渡して呼び出すことで初期化します。作成した一時ファイルをDBとしてSpockアプリケーションを作成しますが、ここではWebアプリケーションサーバを起動する必要はありません。代わりに spockAsApp 関数を使い、WAIミドルウェアからWAIアプリケーションに変換しています。 runSession I/OアクションにテストとWAIアプリケーションを渡すと、そのアプリケーションを対象としてテストを実施します。テストはSessionモナドで記述します。
main :: IO ()
main = void $ runTestTT (TestCase testWeightRecorder)
sqlFile :: IO FilePath
sqlFile = do
datadir <- getDataDir
return $ datadir </> "data" </> "schema.sql"
testWeightRecorder :: Assertion
testWeightRecorder =
withSystemTempFile "test.db" $
\path _ ->
do sql <- sqlFile
callCommand $ "sqlite3 " ++ path ++ " < " ++ sql
datadir <- getDataDir
let cfg =
WRConfig
{ wrcDBPath = path
, wrcTplRoots = [datadir </> "templates"]
, wrcPort = 9999 -- Won't use this value while testing
}
m = weightRecorderMiddleware cfg
WT.runSession basic =<< spockAsApp m
Network.Wai.Test モジュールはテストを記述するためのモジュールであり、wai-extraパッケージに定義されています。テストの記述に用いるSessionモナドは、指定したWAIアプリケーションの呼び出しを提供します。また、HTTPクッキーを保持し、WAIアプリケーションにアクセスする場合には適切にクッキーを送ってくれます。Weight Recorderアプリケーションのようにセッションを扱うWAIアプリケーションをテストする際に、大変便利な機能です。Network.Wai.Test では簡単のため、 WAI の Request と Response をそのままではなく、コンテンツ部分の扱いを単純にした SRequest と SResponse という型を使ってWAIアプリケーションとやり取りします。最後に定義しているrequestはWAIアプリケーションとやり取りするためのラッパーです。リクエストを作るための引数と、レスポンスを受け取ってテストを実施するコールバックを受け取り、アプリケーションを呼び出した結果を元にコールバックを呼び出します。また、 lookup 関数によって Location HTTPヘッダがあるかを確認し、 Location ヘッダが送られてきている場合はリダイレクト先を get 関数で再び呼び出します。 get と post は request アクションのラッパーであり、文字通りHTTPのGETメソッドとPOSTメソッドを使ったWAIアプリケーションの呼び出しを実現します。
get :: BS.ByteString -> (WT.SResponse -> WT.Session ()) -> WT.Session ()
get url = request HTTP.methodGet [] url ""
post
:: BS.ByteString
-> LBS.ByteString
-> (WT.SResponse -> WT.Session ())
-> WT.Session ()
post url body = request HTTP.methodPost postHeaders url body
where
postHeaders = [(HTTP.hContentType, "application/x-www-form-urlencoded")]
request
:: HTTP.Method
-> [HTTP.Header]
-> BS.ByteString
-> LBS.ByteString
-> (WT.SResponse -> WT.Session ())
-> WT.Session ()
request meth hs url body cb = do
let req =
WT.defaultRequest
{ WAI.requestMethod = meth
, WAI.requestHeaders = hs
} `WT.setPath`
url
res <- WT.srequest (WT.SRequest req body)
case lookup HTTP.hLocation (WT.simpleHeaders res) of
Nothing -> cb res
Just url' -> get url' cb
basic がテストの詳細を記述したものです。最初に書いたシナリオに沿って一通りWeight Recorderアプリケーションにアクセスし、 assertBodyContains アクション を使って戻されたHTMLに想定する文字列が含まれるかをチェックしています。
basic :: WT.Session ()
basic = do
get "/" $
\res ->
do bodyContains "ユーザ登録" res
bodyContains "ログイン" res
post "/register" "name=hoge&password=hage" $ bodyContains "登録しました"
post "/login" "name=hoge&password=hage" $ bodyContains "体重の入力"
post "/new_record" "weight=60.1" $ bodyContains "60.1"
where
bodyContains = WT.assertBodyContains . LBS.fromString
単体テストの実行
それでは、Stackを使って単体テストを呼び出します。 weight-recorder.cabal にテストをビルドするための記述を追加します。
test-suite weight-recorder-test
type: exitcode-stdio-1.0
hs-source-dirs: test
main-is: test.hs
build-depends: base
, bytestring
, directory
, filepath
, http-types
, HUnit
, temporary
, utf8-string
, wai
, wai-extra
, weight-recorder
, process
, Spock
ghc-options: -Wall
default-language: Haskell2010
stack test の実行で単体テストを実行できます。
$ stack test
..略..
weight-recorder-0.1.0.0: test (suite: weight-recorder-test)
Cases: 1 Tried: 1 Errors: 0 Failures: 0