Use a different server-running method and add bulleted list of strategies at start

This commit is contained in:
Erik Aker 2018-10-14 10:19:49 -07:00
parent 0d0fd0de82
commit 64f89f600a

View file

@ -7,7 +7,36 @@ In this recipe we'll work through some common testing strategies and provide
examples of utlizing these testing strategies in order to test Servant examples of utlizing these testing strategies in order to test Servant
applications. applications.
This recipe uses the following ingredients: ## Testing strategies
There are many testing strategies you may wish to employ when testing your
Servant application, but included below are three common testing patterns:
- We'll use `servant-client` to derive client functions and then make valid
requests of our API, running in another thread. This is great for testing
that our **business logic** is correctly implemented with only valid HTTP
requests.
- We'll also use `hspec-wai` to make **arbitrary HTTP requests**, in order to
test how our application may respond to invalid or otherwise unexpected
requests.
- Finally, we can also use `servant-quickcheck` for **whole-API tests**, in order
to assert that our entire application conforms to **best practices**.
## Useful Libraries
The following libraries will often come in handy when we decide to test our
Servant applications:
- [hspec](https://hspec.github.io/)
- [hspec-wai](http://hackage.haskell.org/package/hspec-wai)
- [QuickCheck](http://hackage.haskell.org/package/QuickCheck)
- [servant-quickcheck](https://hackage.haskell.org/package/servant-quickcheck)
## Imports and Our Testing Module
This recipe starts the following ingredients:
```haskell ```haskell
{-# LANGUAGE OverloadedStrings, TypeFamilies, DataKinds, {-# LANGUAGE OverloadedStrings, TypeFamilies, DataKinds,
@ -80,10 +109,13 @@ type UserApi =
A real server would likely use a database to store, retrieve, and validate A real server would likely use a database to store, retrieve, and validate
users, but we're going to do something really simple merely to have something users, but we're going to do something really simple merely to have something
to test. With that said, here's a sample handler for the endpoint described to test. With that said, here's a sample handler, server, and `Application`
above: for the endpoint described above:
```haskell ```haskell
userApp :: Application
userApp = serve (Proxy :: Proxy UserApi) userServer
userServer :: Server UserApi userServer :: Server UserApi
userServer = createUser userServer = createUser
@ -107,8 +139,19 @@ of it and see how it responds.
Let's write some tests: Let's write some tests:
```haskell ```haskell
withUserApp :: IO () -> IO ()
withUserApp action =
-- we can spin up a server in another thread and kill that thread when done
-- in an exception-safe way
bracket (liftIO $ C.forkIO $ Warp.run 8888 userApp)
C.killThread
(const action)
businessLogicSpec :: Spec businessLogicSpec :: Spec
businessLogicSpec = do businessLogicSpec =
-- `around` will our Server before the tests and turn it off after
around_ withUserApp $ do
-- create a test client function -- create a test client function
let createUser = client (Proxy :: Proxy UserApi) let createUser = client (Proxy :: Proxy UserApi)
-- create a servant-client ClientEnv -- create a servant-client ClientEnv
@ -116,9 +159,6 @@ businessLogicSpec = do
manager <- runIO $ newManager defaultManagerSettings manager <- runIO $ newManager defaultManagerSettings
let clientEnv = mkClientEnv manager baseUrl let clientEnv = mkClientEnv manager baseUrl
-- Run the server in another thread (`runIO` is from `hspec`)
runIO $ C.forkIO $ Warp.run 8888 (serve (Proxy :: Proxy UserApi) userServer)
-- testing scenarios start here -- testing scenarios start here
describe "POST /user" $ do describe "POST /user" $ do
it "should create a user with a high enough ID" $ do it "should create a user with a high enough ID" $ do
@ -271,8 +311,8 @@ esTestServer = getESDocument
-- here specifically to trigger different behavior in our tests. -- here specifically to trigger different behavior in our tests.
getESDocument :: Integer -> Handler Value getESDocument :: Integer -> Handler Value
getESDocument docId getESDocument docId
-- arbitrary things we can use in our tests to simulate failure -- arbitrary things we can use in our tests to simulate failure:
-- We want to trigger different code paths -- we want to trigger different code paths.
| docId > 1000 = throwError err500 | docId > 1000 = throwError err500
| docId > 500 = pure . Object $ HM.fromList [("bad", String "data")] | docId > 500 = pure . Object $ HM.fromList [("bad", String "data")]
| otherwise = pure $ Object $ HM.fromList [("_source", Object $ HM.fromList [("a", String "b")])] | otherwise = pure $ Object $ HM.fromList [("_source", Object $ HM.fromList [("a", String "b")])]
@ -289,7 +329,7 @@ Hopefully, this will simplify our testing code:
```haskell ```haskell
thirdPartyResourcesSpec :: Spec thirdPartyResourcesSpec :: Spec
thirdPartyResourcesSpec = around_ withElasticsearch $ do thirdPartyResourcesSpec = around_ withElasticsearch $ do
-- we call `with` and pass our own servant-server `Application` -- we call `with` from `hspec-wai` and pass *real* `Application`
with (pure $ serve (Proxy :: Proxy DocApi) $ docServer "localhost" "9999") $ do with (pure $ serve (Proxy :: Proxy DocApi) $ docServer "localhost" "9999") $ do
describe "GET /docs" $ do describe "GET /docs" $ do
it "should be able to get a document" $ it "should be able to get a document" $
@ -450,8 +490,13 @@ another web framework. You have to specify whether you're looking for
There are lots of techniques for testing and we only covered a few here. There are lots of techniques for testing and we only covered a few here.
Useful libraries such as `hspec-wai` have ways of testing Wai `Application`s Useful libraries such as `hspec-wai` have ways of running Wai `Application`s
and sending requests to them, while Servant's type-level DSL for defining APIs and sending requests to them, while Servant's type-level DSL for defining APIs
allows us to more easily mock out servers. Lastly, if you want a broad allows us to more easily mock out servers and to derive clients, which will
overview of where your application fits in with regard to best practices, only craft valid requests.
consider using `servant-quickcheck`.
Lastly, if you want a broad overview of where your application fits in with
regard to best practices, consider using `servant-quickcheck`.
This program is available as a cabal project
[here](https://github.com/haskell-servant/servant/tree/master/doc/cookbook/testing).