このエントリは 前回 に引き続き、本日(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