简体   繁体   English

如何快速检查从 IO 构建的仆人应用程序?

[英]How do I QuickCheck a Servant Application that is constructed from an IO?

I am writing an API server using Servant.我正在使用 Servant 编写 API 服务器。 The server includes persistent state.服务器包括持久状态。 I would like to use QuickCheck to write tests for the server.我想使用 QuickCheck 为服务器编写测试。

The implementation of various endpoints that make up the Servant Application require a database value.构成仆人应用程序的各种端点的实现需要一个数据库值。 Unsurprisingly, creation of the database value is in the IO monad.不出所料,数据库值的创建在IO monad 中。

I don't understand how to combine the pieces from Hspec, Wai, QuickCheck, and Servant in a way that satisfies them all.我不明白如何将 Hspec、Wai、QuickCheck 和 Servant 中的部分组合在一起,以满足他们所有人的需求。

I see that I can perform an IO as part of creating the Hspec Spec itself and I see that I can specify that an IO be performed before each item in the Hspec Spec.我看到我可以在创建 Hspec 规范本身的过程中执行 IO,并且我看到我可以指定在 Hspec 规范中的每个项目之前执行 IO。 Neither of these capabilities seems helpful in this case.在这种情况下,这些功能似乎都没有帮助。 The IO needs to be performed for each QuickCheck iteration of the property.需要为属性的每个 QuickCheck 迭代执行 IO。 Without this, the database accumulates state from each iteration which invalidates the definition of the property (or at least makes it greatly more complicated).没有这个,数据库会从每次迭代中累积状态,这会使属性的定义无效(或者至少使它变得更加复杂)。

Below is my attempt to create a minimal, self-contained example of this scenario.下面是我尝试创建此场景的最小、自包含示例。

{-# 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'

This doesn't type check:这不会类型检查:

/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) }
    |   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

I don't know if there is a way to fit a WaiExpectation into a PropertyM IO () at all.我不知道是否有办法将WaiExpectation放入PropertyM IO ()中。 I don't even know if monadicIO is helpful here at all.我什至不知道monadicIO在这里是否有帮助。

How can I fit these pieces together?我怎样才能将这些部分组合在一起?

Define spec_g :: Background -> Spec , then take advantage of IO 's Functor and Monad instances.定义spec_g :: Background -> Spec ,然后利用IOFunctorMonad实例。

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

or more concisely,或者更简洁地说,

main = spec_g <$> openBackend 16 >>= hspec

IIRC, you're supposed to run each spec or property with the with function. IIRC,您应该使用with函数运行每个规范或属性。 Here's a few properties I wrote some time ago:下面是我前段时间写的几个属性:

  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

The app value serves the service, and as far as I recall, it runs the IO action for each test. app值为服务提供服务,据我所知,它为每个测试运行IO操作。 I did it with an in-memory database using an IORef , and that seems to be working just fine:我使用IORef使用内存数据库完成了IORef ,这似乎工作得很好:

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

The WQC.property function is from a qualified import: WQC.property函数来自合格的导入:

import qualified Test.Hspec.Wai.QuickCheck as WQC

I wasn't too happy, however, with the way I had to structure my tests and properties with HSpec, so I ultimately rewrote all the tests to be driven by HUnit.然而,我对使用 HSpec 构建测试和属性的方式不太满意,所以我最终重写了所有测试以由 HUnit 驱动。 I've an upcoming blog post that describes this, but I haven't published it yet.我有一篇即将发布的博客文章描述了这一点,但我还没有发布。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM