1

Servant を使用して API サーバーを作成しています。サーバーには永続的な状態が含まれます。QuickCheck を使用してサーバーのテストを作成したいと考えています。

サーバント アプリケーションを構成するさまざまなエンドポイントの実装には、データベース値が必要です。当然のことながら、データベース値の作成はIOモナドにあります。

Hspec、Wai、QuickCheck、および Servant のピースをすべてを満たす方法で組み合わせる方法がわかりません。

Hspec 仕様自体の作成の一部として IO を実行できることがわかりました。また、Hspec 仕様の各項目の前に IO を実行するように指定できることがわかりました。この場合、これらの機能はどちらも役に立たないようです。プロパティの QuickCheck 反復ごとに IO を実行する必要があります。これがないと、データベースは反復ごとに状態を蓄積するため、プロパティの定義が無効になります (または、少なくとも非常に複雑になります)。

以下は、このシナリオの最小限の自己完結型の例を作成する試みです。

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleContexts #-}

module Main where

import Data.IORef
import Test.QuickCheck
import Test.QuickCheck.Monadic
import qualified Test.Hspec.Wai.QuickCheck as QuickWai
import Test.Hspec
import Test.Hspec.Wai
import Text.Printf
import Servant
import Servant.API
import Data.Aeson
import Data.Text.Encoding
import Data.ByteString.UTF8
  ( fromString
  )

data Backend = Backend (IORef Integer)

openBackend :: Integer -> IO Backend
openBackend n = Backend <$> newIORef n

data Acknowledgement = Ok Integer

instance ToJSON Acknowledgement where
  toJSON (Ok n) = object [ "value" .= n ]

serveSomeNumber :: Backend -> Integer -> IO Acknowledgement
serveSomeNumber (Backend a) b = do
  a' <- readIORef a
  modifyIORef a (\n -> n + 1)
  return $ Ok (a' + b)

type TheAPI = Capture "SomeNumber" Integer :> Post '[JSON] Acknowledgement

theServer :: Backend -> Server TheAPI
theServer backend = liftIO . serveSomeNumber backend

theAPI :: Proxy TheAPI
theAPI = Proxy

app :: Backend -> Application
app backend = serve theAPI (theServer backend)

post' n =
  let
    url = printf "/%d" (n :: Integer)
    encoded = fromString url
  in
    post encoded ""

spec_g :: Backend -> Spec
spec_g (Backend expectedResult) =
  describe "foo" $
  it "bar" $ property $ \genN -> monadicIO $ do
  n <- run genN
  m <- run $ readIORef expectedResult
  post' n `shouldRespondWith` ResponseMatcher { matchStatus = fromInteger (n + m) }

main :: IO ()
main = do
  spec_g' <- spec_g `fmap` openBackend 16
  hspec spec_g'

これは型チェックを行いません:

/home/exarkun/Scratch/QuickCheckIOApplication/test/Spec.hs:119:3: error:
    * Couldn't match type `WaiSession' with `PropertyM IO'
      Expected type: PropertyM IO ()
        Actual type: WaiExpectation
    * In a stmt of a 'do' block:
        post' n
          `shouldRespondWith`
            ResponseMatcher {matchStatus = fromInteger (n + m)}
      In the second argument of `($)', namely
        `do n <- run genN
            m <- run $ readIORef expectedResult
            post' n
              `shouldRespondWith`
                ResponseMatcher {matchStatus = fromInteger (n + m)}'
      In the expression:
        monadicIO
          $ do n <- run genN
               m <- run $ readIORef expectedResult
               post' n
                 `shouldRespondWith`
                   ResponseMatcher {matchStatus = fromInteger (n + m)}
    |
119 |   post' n `shouldRespondWith` ResponseMatcher { matchStatus = fromInteger (n + m) }
    |   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

WaiExpectationaを aに適合させる方法があるかどうかはわかりませんPropertyM IO ()monadicIOここで役立つかどうかさえわかりません。

これらの部品をどのように組み合わせることができますか?

4

2 に答える 2

0

IIRC では、各仕様またはプロパティをwith関数で実行することになっています。少し前に書いたいくつかのプロパティを次に示します。

  with app $ describe "/reservations/" $ do
    it "responds with 404 when no reservation exists" $ WQC.property $ \rid ->
      get ("/reservations/" <> toASCIIBytes rid) `shouldRespondWith` 404

    it "responds with 200 after reservation is added" $ WQC.property $ \
      (ValidReservation r) -> do
      _ <- postJSON "/reservations" $ encode r
      let actual = get $ "/reservations/" <> toASCIIBytes (reservationId r)
      actual `shouldRespondWith` 200

値はサービスを提供し、app私が思い出す限り、IO各テストのアクションを実行します。を使用してメモリ内データベースでそれを行いましたが、それはうまく機能しIORefているようです:

app :: IO Application
app = do
  ref <- newIORef Map.empty
  return $
    serve api $
    hoistServer api (Handler . runInFakeDBAndIn2019 ref) $
    server 150 []

WQC.property関数は、修飾されたインポートからのものです。

import qualified Test.Hspec.Wai.QuickCheck as WQC

しかし、HSpec を使用してテストとプロパティを構造化しなければならなかった方法にはあまり満足できなかったので、最終的にはすべてのテストを HUnit によって駆動されるように書き直しました。これについて説明する次のブログ投稿がありますが、まだ公開していません。

于 2019-09-03T21:20:59.673 に答える