servant-quickcheck/doc/posts/announcement.md
2016-05-01 14:29:47 +02:00

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»