3.5 KiB
Announcing servant-quickcheck
Some time ago, we released servant-mock
. The idea behind it is to use
QuickCheck
to create a mock server that accords with a servant API. Not long
after, we started thinking about an analog that would, instead of mocking a
server, mock a client instead - i.e., generate random requests that conform to
an API description.
This is much closer to the traditional use of QuickCheck
. The most obvious
use-case is checking that properties hold of an entire server rather than of
individual endpoints.
serverSatisfies
There are a variety of best practices in writing web APIs that aren't always obvious. As a running example, let's use a simple service that allows adding, removing, and querying biological species. Our SQL schema is:
«schema.sql»
CREATE TABLE genus ( genus_name text PRIMARY KEY, genus_family text NOT NULL ) CREATE TABLE species ( species_name text PRIMARY KEY, species_genus text NOT NULL REFERENCES genus (genus_name) )
And our actual application:
«Main.hs»
{-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} import Servant import Data.Aeson import Database.PostgreSQL.Simple import GHC.Generics (Generic) import Data.Text (Text) import Network.Wai.Handler.Warp import Control.Monad.IO.Class (liftIO) type API = "species" :> (Capture "species-name" Text :> ( Get '[JSON] Species :<|> Delete '[JSON] ()) :<|> ReqBody '[JSON] Species :> Post '[JSON] () :<|> "count" :> Get '[JSON] Int) api :: Proxy API api = Proxy data Species = Species { speciesName :: Text , speciesGenus :: Text } deriving (Eq, Show, Read, Generic, ToJSON, FromJSON) data Genus = Genus { genusName :: Text , genusFamily :: Text } deriving (Eq, Show, Read, Generic, ToJSON, FromJSON) instance FromRow Genus instance FromRow Species server :: Connection -> Server API server conn = (\sname -> liftIO (lookupSpecies conn sname) :<|> liftIO (deleteSpecies conn sname)) :<|> (\species -> liftIO $ insertSpecies conn species) :<|> (liftIO $ countSpecies conn) lookupSpecies :: Connection -> Text -> IO Species lookupSpecies conn name = do [s] <- query conn "SELECT * FROM species WHERE species_name == ?" (Only name) return s deleteSpecies :: Connection -> Text -> IO () deleteSpecies conn name = do _ <- execute conn "DELETE FROM species WHERE species_name == ?" (Only name) return () insertSpecies :: Connection -> Species -> IO () insertSpecies conn Species{..} = do _ <- execute conn "INSERT INTO species (species_name, species_genus) VALUES (?)" (speciesName, speciesGenus) return () countSpecies :: Connection -> IO Int countSpecies conn = do [Only count] <- query_ conn "SELECT count(*) FROM species" return count main :: IO () main = do conn <- connectPostgreSQL "" run 8090 (serve api $ server conn)
» Main.hs
«Main.hs»
» schema.sql
«schema.sql»