YesodでのFunctional Test

追記:2012/04/05: yesod-testがリリースされた。http://hackage.haskell.org/package/yesod-test

YesodはWai上に実装されているので、機能テストにはwai-testが使用できる。で、Yesod自身のテストコードはどうなってるのかなと見たところ、どうやらHspecと併用して要件を満たしているようで、同じテスト構成で構築したので備忘録がわりにメモ。

Yesodのやり方に従ったのでHspecという選択肢にしたが、別にdoctestでもQuickCheckでもなんでもいい。

├── Application.hs
├── Foundation.hs
├── Handler
│   ├── Api
│   │   └── User.hs
│   └── Root.hs
├── Import.hs
├── LICENSE
├── Sample.cabal
├── Settings
│   └── StaticFiles.hs
├── Settings.hs
├── Tests
│   ├── SampleTest
│   │   └── Handler
│   │       └── Api
│   │           └── User.hs
│   ├── SampleTest.hs
│   └── test.hs
<以下省略>

.cabalの編集。extra-source-filesにTestsディレクトリ以下のファイルを追加。Flag testの追加。testフラグが立っている場合はwai-testを使用。

<省略>
cabal-version:     >= 1.6
build-type:        Simple
homepage:          http://Sample.yesodweb.com/
extra-source-files:
    Tests/SampleTest/Handler/Api/User.hs
    Tests/SampleTest.hs
    Tests/test.hs

Flag test
  description: Build the executable to run unit tests
  default: False

Flag dev
    Description:   Turn on development settings, like auto-reload templates.
    Default:       False

Flag library-only
    Description:   Build for use with "yesod devel"
    Default:       False

library
    if flag(library-only)
        Buildable: True
    else
        Buildable: False

    if flag(test)
        build-depends: wai-test
<省略>

.cabalにtest-suiteを追加。

test-suite tests
    type: exitcode-stdio-1.0
    main-is: Tests/test.hs
    cpp-options: -DTEST
    extensions: TemplateHaskell
                QuasiQuotes
                OverloadedStrings
                NoImplicitPrelude
                CPP
                OverloadedStrings
                MultiParamTypeClasses
                TypeFamilies
    build-depends: base                          >= 4          && < 5                  , yesod                         >= 0.10.1     && < 0.11                  , yesod-core                    >= 0.10       && < 0.11                  , yesod-static                  >= 0.10       && < 0.11                  , yesod-default                 >= 0.6        && < 0.7                  , clientsession                 >= 0.7.3      && < 0.8                  , bytestring                    >= 0.9        && < 0.10                  , text                          >= 0.11       && < 0.12                  , template-haskell                  , hamlet                        >= 0.10       && < 0.11                  , shakespeare-text              >= 0.10       && < 0.11                  , shakespeare-css               >= 0.10.7.1   && < 0.11                  , shakespeare-js                >= 0.11.1     && < 0.12                  , wai                           >= 1.1        && < 1.2                  , wai-extra                     >= 1.1        && < 1.2                  , transformers                  >= 0.2        && < 0.3                  , monad-control                 >= 0.3        && < 0.4                  , yaml                          >= 0.5        && < 0.6                  , blaze-html                    >=0.4.3.1     && < 0.5                  , SHA                           >=1.5.0.0     && < 1.6.0.0                  , utf8-string                   >=0.3.7       && < 0.4                  , old-time                      >=1.1.0.0     && < 1.2                  , QuickCheck                    >=2.4.2       && < 3.0                  , random                        >=1.0.1.1     && < 1.1                  , test-framework                >=0.5         && < 0.6                  , test-framework-hunit          >=0.2.7       && < 0.3                  , test-framework-quickcheck2    >=0.2.12      && < 0.3                  , HUnit                  , hspec                         >=0.9.1.1      && < 1.0                  , wai-test                      >=1.1.1        && < 2.0                  , wai                  , wai-extra                     >=1.1.0.1      && < 1.2                  , cookie                        >=0.4.0        && < 0.5                  , http-conduit                  >=1.2.6        && < 1.3                  , clientsession                 >=0.7.4        && < 0.8                  , aeson                         >= 0.5
    ghc-options: -Wall

Tests/test.hs にエントリポイントを記述。

import Import
import Test.Hspec
import Tests.SampleTest

main :: IO ()
main = hspecX $ descriptions $ specs

Tests/SampleTest.hs で[Specs]を作成。

module Tests.SampleTest (specs) where

import Tests.SampleTest.Handler.Api.User
import Test.Hspec

specs :: [Specs]
specs =
    [ userTest
    ]

Tests/SampleTest/Handler/Api/User.hs

module Tests.SampleTest.Handler.Api.User (userTest) where

import Prelude
import Yesod
import Application
import Test.Hspec
import Test.Hspec.HUnit ()
import Network.Wai
import Network.Wai.Test

userTest :: [Spec]
userTest = describe "SampleTest"
    [ it "check request is valid" case_request_valid
    ]

getApp :: IO Application
getApp = do
    (_,app) <- getApplicationDev     return app runner :: Session () -> IO ()
runner f = getApp >>= runSession f

case_request_valid :: IO ()
case_request_valid = runner $ do
      res <- request defaultRequest
        {
          pathInfo = ["api", "user"],
          requestHeaders = [("Accept-Language", "es")]
        }
      assertBody "aaa" res

getApplicationDevからApplicationを取ってきてrunSessionに投げる関数を作り、テストケース内でリクエストを投げてテストを実行していく。

実行。

cabal configure -ftest --enable-tests
cabal build
cabal test
Written on March 1, 2012