1

我正在使用 Servant 编写 API 服务器。服务器包括持久状态。我想使用 QuickCheck 为服务器编写测试。

构成仆人应用程序的各种端点的实现需要一个数据库值。毫不奇怪,数据库值的创建是在IOmonad 中。

我不明白如何将 Hspec、Wai、QuickCheck 和 Servant 中的部分组合在一起,以使它们都满足。

我看到我可以执行 IO 作为创建 Hspec Spec 本身的一部分,并且我看到我可以指定在 Hspec Spec 中的每个项目之前执行 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) }
    |   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

我不知道是否有办法将 aWaiExpectation放入 aPropertyM IO ()中。我什至不知道monadicIO这里是否有帮助。

我怎样才能把这些碎片拼在一起?

4

2 回答 2

1

定义spec_g :: Background -> Spec,然后利用IO'FunctorMonad实例。

main = do
    spec <- fmap spec_g (openBackend 16) -- fmap spec_g :: IO Background -> IO Spec
    hspec spec

或更简洁地说,

main = spec_g <$> openBackend 16 >>= hspec
于 2019-09-03T19:28:46.350 回答
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 回答