@@ -7,7 +7,36 @@ In this recipe we'll work through some common testing strategies and provide
7examples of utlizing these testing strategies in order to test Servant
7examples of utlizing these testing strategies in order to test Servant
8applications.
8applications.
12+There are many testing strategies you may wish to employ when testing your
13+Servant application, but included below are three common testing patterns:
15+- We'll use `servant-client` to derive client functions and then make valid
16+requests of our API, running in another thread. This is great for testing
17+that our **business logic** is correctly implemented with only valid HTTP
20+- We'll also use `hspec-wai` to make **arbitrary HTTP requests**, in order to
21+test how our application may respond to invalid or otherwise unexpected
24+- Finally, we can also use `servant-quickcheck` for **whole-API tests**, in order
25+to assert that our entire application conforms to **best practices**.
29+The following libraries will often come in handy when we decide to test our
32+- [hspec](https://hspec.github.io/)
33+- [hspec-wai](http://hackage.haskell.org/package/hspec-wai)
34+- [QuickCheck](http://hackage.haskell.org/package/QuickCheck)
35+- [servant-quickcheck](https://hackage.haskell.org/package/servant-quickcheck)
37+## Imports and Our Testing Module
10−This recipe uses the following ingredients:
39+This recipe starts the following ingredients:
13{-# LANGUAGE OverloadedStrings, TypeFamilies, DataKinds,
42{-# LANGUAGE OverloadedStrings, TypeFamilies, DataKinds,
@@ -80,10 +109,13 @@ type UserApi =
81A real server would likely use a database to store, retrieve, and validate
110A real server would likely use a database to store, retrieve, and validate
82users, but we're going to do something really simple merely to have something
111users, but we're going to do something really simple merely to have something
83−to test. With that said, here's a sample handler for the endpoint described
112+to test. With that said, here's a sample handler, server, and `Application`
84−above:
113+for the endpoint described above:
86```haskell
115```haskell
116+userApp :: Application
117+userApp = serve (Proxy :: Proxy UserApi) userServer
87userServer :: Server UserApi
119userServer :: Server UserApi
88userServer = createUser
120userServer = createUser
@@ -107,26 +139,34 @@ of it and see how it responds.
107Let's write some tests:
139Let's write some tests:
109```haskell
141```haskell
142+withUserApp :: IO () -> IO ()
144+ -- we can spin up a server in another thread and kill that thread when done
145+ -- in an exception-safe way
146+ bracket (liftIO $ C.forkIO $ Warp.run 8888 userApp)
110businessLogicSpec :: Spec
151businessLogicSpec :: Spec
111−businessLogicSpec = do
152+businessLogicSpec =
153+ -- `around` will our Server before the tests and turn it off after
154+ around_ withUserApp $ do
112− -- create a test client function
155+ -- create a test client function
113− let createUser = client (Proxy :: Proxy UserApi)
156+ let createUser = client (Proxy :: Proxy UserApi)
114− -- create a servant-client ClientEnv
157+ -- create a servant-client ClientEnv
115− baseUrl <- runIO $ parseBaseUrl "http://localhost:8888"
158+ baseUrl <- runIO $ parseBaseUrl "http://localhost:8888"
116− manager <- runIO $ newManager defaultManagerSettings
159+ manager <- runIO $ newManager defaultManagerSettings
117− let clientEnv = mkClientEnv manager baseUrl
160+ let clientEnv = mkClientEnv manager baseUrl
119− -- Run the server in another thread (`runIO` is from `hspec`)
120− runIO $ C.forkIO $ Warp.run 8888 (serve (Proxy :: Proxy UserApi) userServer)
122− -- testing scenarios start here
162+ -- testing scenarios start here
123− describe "POST /user" $ do
163+ describe "POST /user" $ do
124− it "should create a user with a high enough ID" $ do
164+ it "should create a user with a high enough ID" $ do
125− result <- runClientM (createUser 50001) clientEnv
165+ result <- runClientM (createUser 50001) clientEnv
126− result `shouldBe` (Right $ User { name = "some user", user_id = 50001})
166+ result `shouldBe` (Right $ User { name = "some user", user_id = 50001})
127− it "will it fail with a too-small ID?" $ do
167+ it "will it fail with a too-small ID?" $ do
128− result <- runClientM (createUser 4999) clientEnv
168+ result <- runClientM (createUser 4999) clientEnv
129− result `shouldBe` (Right $ User { name = "some user", user_id = 50001})
169+ result `shouldBe` (Right $ User { name = "some user", user_id = 50001})
132### Running These Tests
172### Running These Tests
@@ -271,8 +311,8 @@ esTestServer = getESDocument
271-- here specifically to trigger different behavior in our tests.
311-- here specifically to trigger different behavior in our tests.
272getESDocument :: Integer -> Handler Value
312getESDocument :: Integer -> Handler Value
273getESDocument docId
313getESDocument docId
274− -- arbitrary things we can use in our tests to simulate failure
314+ -- arbitrary things we can use in our tests to simulate failure:
275− -- We want to trigger different code paths
315+ -- we want to trigger different code paths.
276 | docId > 1000 = throwError err500
316 | docId > 1000 = throwError err500
277 | docId > 500 = pure . Object $ HM.fromList [("bad", String "data")]
317 | docId > 500 = pure . Object $ HM.fromList [("bad", String "data")]
278 | otherwise = pure $ Object $ HM.fromList [("_source", Object $ HM.fromList [("a", String "b")])]
318 | otherwise = pure $ Object $ HM.fromList [("_source", Object $ HM.fromList [("a", String "b")])]
@@ -289,7 +329,7 @@ Hopefully, this will simplify our testing code:
289```haskell
329```haskell
290thirdPartyResourcesSpec :: Spec
330thirdPartyResourcesSpec :: Spec
291thirdPartyResourcesSpec = around_ withElasticsearch $ do
331thirdPartyResourcesSpec = around_ withElasticsearch $ do
292− -- we call `with` and pass our own servant-server `Application`
332+ -- we call `with` from `hspec-wai` and pass *real* `Application`
293 with (pure $ serve (Proxy :: Proxy DocApi) $ docServer "localhost" "9999") $ do
333 with (pure $ serve (Proxy :: Proxy DocApi) $ docServer "localhost" "9999") $ do
294 describe "GET /docs" $ do
334 describe "GET /docs" $ do
295 it "should be able to get a document" $
335 it "should be able to get a document" $
@@ -450,8 +490,13 @@ another web framework. You have to specify whether you're looking for
451There are lots of techniques for testing and we only covered a few here.
491There are lots of techniques for testing and we only covered a few here.
453−Useful libraries such as `hspec-wai` have ways of testing Wai `Application`s
493+Useful libraries such as `hspec-wai` have ways of running Wai `Application`s
454and sending requests to them, while Servant's type-level DSL for defining APIs
494and sending requests to them, while Servant's type-level DSL for defining APIs
455−allows us to more easily mock out servers. Lastly, if you want a broad
495+allows us to more easily mock out servers and to derive clients, which will
496+only craft valid requests.
456−overview of where your application fits in with regard to best practices,
498+Lastly, if you want a broad overview of where your application fits in with
457−consider using `servant-quickcheck`.
499+regard to best practices, consider using `servant-quickcheck`.
501+This program is available as a cabal project
502+[here](https://github.com/haskell-servant/servant/tree/master/doc/cookbook/testing).