on master help sign in
WIP Commit: must finish servant-quickcheck tests
43af1d0Erik Aker
cabal.project+1
@@ -19,6 +19,7 @@ packages: servant/
19 doc/cookbook/jwt-and-basic-auth/
19 doc/cookbook/jwt-and-basic-auth/
20 doc/cookbook/pagination
20 doc/cookbook/pagination
21 doc/cookbook/sentry
21 doc/cookbook/sentry
22+ doc/cookbook/testing
22 doc/cookbook/structuring-apis
23 doc/cookbook/structuring-apis
23 doc/cookbook/using-custom-monad
24 doc/cookbook/using-custom-monad
24 doc/cookbook/using-free-client
25 doc/cookbook/using-free-client
doc/cookbook/cabal.project+1
@@ -11,6 +11,7 @@ packages:
11 https/
11 https/
12 pagination/
12 pagination/
13 sentry/
13 sentry/
14+ testing/
14 ../../servant
15 ../../servant
15 ../../servant-server
16 ../../servant-server
16 ../../servant-client-core
17 ../../servant-client-core
doc/cookbook/index.rst+1
@@ -31,3 +31,4 @@ you name it!
31 pagination/Pagination.lhs
31 pagination/Pagination.lhs
32 curl-mock/CurlMock.lhs
32 curl-mock/CurlMock.lhs
33 sentry/Sentry.lhs
33 sentry/Sentry.lhs
34+ testing/Testing.lhs
doc/cookbook/testing/Testing.lhsnew file+331
@@ -0,0 +1,331 @@
1+# How To Test Servant Applications
2+
3+Even with a nicely structured API that passes Haskell's strict type checker,
4+it's a good idea to write some tests for your application.
5+
6+In this recipe we'll work through some common testing strategies and provide
7+examples of utlizing these testing strategies in order to test Servant
8+applications.
9+
10+This recipe uses the following ingredients:
11+
12+```haskell
13+{-# LANGUAGE OverloadedStrings, TypeFamilies, DataKinds,
14+ DeriveGeneric, TypeOperators #-}
15+import qualified Control.Concurrent as C
16+import Data.Aeson
17+import Data.Aeson.Lens
18+import Data.Text
19+import Network.Wai
20+import qualified Network.Wai.Handler.Warp as Warp
21+import Servant
22+import Servant.Client
23+import Servant.Server
24+
25+import Test.Hspec
26+import Test.Hspec.Wai
27+import Test.Hspec.Wai.Matcher
28+```
29+
30+We're going to produce different `Spec`s that represent different
31+aspects of our application, and we'll ask `hspec` to run all of our different
32+`Spec`s. This is a common organizational method for testing modules:
33+
34+```haskell
35+spec :: Spec
36+spec =
37+ businessLogicSpec
38+ thirdPartyResourcesSpec
39+ servantQuickcheckSpec
40+```
41+
42+Often, codebases will use `hspec`'s
43+[autodiscover pragma](http://hspec.github.io/hspec-discover.html)
44+to find all testing modules and `Spec`s inside, but we're going to
45+explicitly make a `main` function to run our tests because we have only one
46+`spec` defined above:
47+
48+```haskell
49+main :: IO ()
50+main = hspec spec
51+```
52+
53+## Testing Your Business Logic
54+
55+Let's say we have an API that looks something like this:
56+
57+```haskell
58+data User = User {
59+ name :: Text
60+ , user_id :: Integer
61+ } deriving (Eq, Show, Generic)
62+
63+instance FromJSON User
64+instance ToJSON User
65+
66+type UserApi =
67+ -- One endpoint: create a user
68+ "user" :> Capture "userId" Integer :> Post '[JSON] User
69+```
70+
71+A real server would likely use a database to store, retrieve, and validate
72+users, but we're going to do something really simple merely to have something
73+to test. With that said, here's a sample handler for the endpoint described
74+above:
75+
76+```haskell
77+userServer :: Server UserApi Handler
78+userServer = createUser
79+ where creatUser userId
80+ | userId > 5000 = pure $ User { name = "some user", user_id = userId }
81+ | otherwise = throwError $ err400 { errBody = "userId is too small" }
82+```
83+
84+### Strategy 1: Spin Up a Server, Create a Client, Make Some Requests
85+
86+One of the benefits of Servant's type-level DSL for describing APIs is that
87+once you have provided a type-level description of your API, you can create
88+clients, documentation, or other tools for it somewhat magically.
89+
90+In this case, we'd like to *test* our server, so we can use `servant-client`
91+to create a client, after which we'll run our server, and then make requests
92+of it and see how it responds.
93+
94+Let's write some tests:
95+
96+```haskell
97+businessLogicSpec :: Spec
98+businessLogicSpec = do
99+ -- create a test client function
100+ createUser = client (Proxy :: Proxy UserApi)
101+ -- create a servant-client ClientEnv
102+ baseUrl <- parseBaseUrl "http://localhost:8888"
103+ manager <- newManager defaultManagerSettings
104+ let clientEnv = mkClientEnv manager baseUrl
105+
106+ -- Run the server in another thread.
107+ liftIO $ C.forkIO $ Warp.run 8888 (server userServer)
108+
109+ -- testing scenarios start here
110+ describe "POST /user" $ do
111+ it "should create a user with a high enough ID" $
112+ result <- runClientM clientEnv (createUser 50001)
113+ result `shouldEqual` Right $ User { name = "some_user", user_id = "5001 "}
114+ it "should fail with a too-small ID" $
115+ result <- runClientM clientEnv (createUser 4999)
116+ result `shouldEqual` Right $ User { name = "some_user", user_id = "5001 "}
117+```
118+
119+### Running These Tests
120+
121+Let's run our tests and see what happens:
122+
123+```
124+$ cabal new-test all
125+```
126+
127+Great: we passed! Servers obviously get more complex, though, and we may not
128+wish to create a whole suite of clients for our server every time. In our next
129+scenario we're going to mock out a 3rd-party resource that our server talks to
130+and we're going to be using `hspec-wai` to run our `Application` instance and
131+to make requests.
132+
133+## *Mocking* 3rd Party Resources
134+
135+Often our web applications will need to make their own web
136+requests to other 3rd-party applications. These requests provide a lot
137+of opportunity for failure and so we'd like to test that the right
138+messages and failure values (in addition to success values) are returned
139+from our application.
140+
141+### Define the 3rd-Party Resource
142+
143+With Servant's type-level API definitions, assuming you've already defined the
144+API you want to mock, it's relatively trivial to create a simple server for
145+the purposes of running tests. For instance, consider an API server that needs
146+to get data out of Elasticsearch. Let's first define the Elasticsearch server
147+and client using Servant API descriptions:
148+
149+```haskell
150+type SearchAPI =
151+ -- We're using Aeson's Generic JSON `Value` to make things easier on
152+ -- ourselves. We're also representing only one Elasticsearch endpoint:
153+ -- get item by id
154+ "myIndex" :> "myDocType" :> Capture "docId" Integer :> Get '[JSON] Value
155+
156+-- Here's our Servant Client function
157+getDocument = client (Proxy :: Proxy SearchAPI)
158+
159+-- We can use these helpers when we want to make requests
160+-- using our client function
161+clientEnv :: Text -> Text -> IO ClientEnv
162+clientEnv esHost esPort = do
163+ baseUrl <- parseBaseUrl $ T.unpack $ esHost <> ":" <> esPort
164+ manager <- newManager defaultManagerSettings
165+ pure $ mkClientEnv manager baseUrl
166+
167+runSearchClient :: Text -> Text -> ClientM a -> IO (Either ServantError a)
168+runSearchClient esHost esPort = (clientEnv esHost esPort >>=) . runClientM
169+```
170+
171+### Servant Server Example Using this 3rd-Party Resource
172+
173+So we've got an Elasticsearch server and a client to talk to it with one
174+function that retrieves a document by its id. Let's now build a simple app
175+server that uses this client to retrieve documents. This is somewhat
176+contrived, but hopefully it illustrates the typical three-tier application
177+architecture.
178+
179+One note: we're also going to take advantage of `aeson-lens` here, which may
180+look a bit foreign. The gist of it is that we're going to traverse a JSON
181+`Value` from Elasticsearch and try to extract some kind of document to
182+return.
183+
184+```haskell
185+type DocApi =
186+ "docs" :> Capture "docId" Integer :> Get '[JSON] Value
187+
188+docServer :: Text -> Text -> Server DocApi Handler
189+docServer esHost esPort = getDocById esHost esPort
190+
191+-- Our Handler tries to get a doc from Elasticsearch and then tries to parse
192+-- it. Unfortunately, there's a lot of opportunity for failure in these
193+-- actions
194+getDocById :: Text -> Text -> Integer -> Handler Value
195+getDocById esHost esPort docId = do
196+ -- Our Servant Client function returns Either ServantError Value here:
197+ docRes <- liftIO $ runSearchClient esHost esPort (getDocument docId)
198+ case docRes of
199+ Left err -> throwError $ err404 { errBody = "Failed looking up content" }
200+ Right value -> do
201+ -- we'll either fail to parse our document or we'll return it
202+ case value ^? _Object . ix "_source" of
203+ Nothing -> throwError $ err400 { errBody = "Failed parsing content" }
204+ Just obj -> pure obj
205+```
206+
207+### Testing Our Backend
208+
209+So the above represents our application. How shall we test this application?
210+Ideally, we'd like it to make requests of a real Elasticsearch server, but we
211+certainly don't want our tests to trigger requests to a live, production
212+database. In addition, we don't want to depend on our real Elasticsearch
213+server having specific, consistent results for us to test against, because
214+that would make our tests flaky (and flaky tests are sometimes described as
215+worse than not having tests at all).
216+
217+One solution to this is to create a trivial Elasticsearch server as part of
218+our testing code. We can do this relatively easily because we already have
219+an API definition for it above. With a *real* server, we can then let our own
220+application make requests of it and we'll simulate different scenarios in
221+order to make sure our application responds the way we expect it to.
222+
223+Let's start with some helpers which will allow us to run a testing version
224+of our Elasticsearch server in another thread:
225+
226+```haskell
227+-- | We'll run the Elasticsearch server so we can test behaviors
228+withElasticsearch :: IO () -> IO ()
229+withElasticsearch action =
230+ bracket (liftIO $ C.forkIO $ Warp.run 9999 esTestApp)
231+ C.killThread
232+ (const action)
233+
234+esTestApp :: Application
235+esTestApp = serve (Proxy :: Proxy SearchAPI) esTestServer
236+
237+esTestServer :: Server SearchAPI Handler
238+esTestServer = getESDocument
239+
240+getESDocument :: Integer -> Handler Value
241+getESDocument docId
242+ -- arbitrary things we can trigger in our tests to check for failure
243+ -- We want to try to trigger different code paths
244+ | docId > 1000 = pure . Left $ ConnectionError "Bad connection!"
245+ | docId > 500 = pure . Object $ HM.fromList [("bad", String "data")]
246+ | otherwise = pure $ Object $ HM.fromList [("_source", defaultDocument)]
247+```
248+
249+Now, we should be ready to write some tests. As mentioned above we're going
250+to use `hspec-wai` in this example to make test requests and to run our own
251+application. This should simplify our testing code somewhat:
252+
253+```haskell
254+thirdPartyResourcesSpec :: Spec
255+thirdPartyResourcesSpec = around_ withElasticsearch $ do
256+ -- we call `with` and pass our servant-server `Application`
257+ with (pure $ serve $ docServer "localhost" "9999") $ do
258+ describe "GET /docs" $ do
259+ it "should be able to get a document" $
260+ -- `get` is a function from hspec-wai`.
261+ get "/docs/1" `shouldRespondWith` 200
262+ it "should be able to handle connection failures" $
263+ -- We can also make custom HTTP requests with the `request` function
264+ get "/docs/1001" `shouldRespondWith` 404
265+ it "should be able to handle parsing failures" $
266+ get "/docs/501" `shouldRespondWith` 400
267+ it "should be able to handle odd HTTP requests" $
268+ -- we can also make all kinds of arbitrary custom requests to see how
269+ -- our server responds using the `request` function:
270+ -- request :: Method -> ByteString -> [Header]
271+ -- -> LB.ByteString -> WaiSession SResponse
272+ request methodPost "/docs/501" [] "{" `shouldRespondWith` 415
273+```
274+
275+
276+## Servant Quickcheck
277+
278+[`servant-quickcheck`](https://github.com/haskell-servant/servant-quickcheck)
279+is a project that allows users to write tests for whole Servant APIs using
280+quickcheck-style property-checking mechanisms.
281+
282+`servant-quickcheck` is great for asserting whole-API rules, such as "no
283+endpoint throws a 500" or "all 301 status codes also come with a Location
284+header". The project even comes with a number of predicates that reference
285+the [RFCs they originate from](https://github.com/haskell-servant/servant-quickcheck/blob/master/src/Servant/QuickCheck/Internal/Predicates.hs).
286+
287+
288+### Quickcheckable API
289+
290+Let's make an API and a server to demonstrate how to use `servant-quickcheck`:
291+
292+```haskell
293+type API = ReqBody '[JSON] String :> Post '[JSON] String
294+ :<|> Get '[JSON] Int
295+ :<|> BasicAuth "some-realm" () :> Get '[JSON] ()
296+
297+api :: Proxy API
298+api = Proxy
299+
300+server :: IO (Server API)
301+server = do
302+ mvar <- newMVar ""
303+ return $ (\x -> liftIO $ swapMVar mvar x)
304+ :<|> (liftIO $ readMVar mvar >>= return . length)
305+ :<|> (const $ return ())
306+```
307+
308+### Using `servant-quickcheck`
309+
310+`servant-quickcheck` also has a cool mechanism where you can compare two API
311+servers to demonstrate that they respond identically to requests. This may be
312+useful if you are planning to rewrite one API in another language or with
313+another web framework. You have to specify whether you're looking for
314+`jsonEquality` vs regular `ByteString` equality, though:
315+
316+```haskell
317+servantQuickcheckSpec :: Spec
318+servantQuickcheckSpec = describe "" $ do
319+ it "API demonstrates best practices" $
320+ withServantServer api server $ \burl ->
321+ serverSatisfies api burl args (unauthorizedContainsWWWAuthenticate
322+ <%> not500
323+ <%> onlyJsonObjects
324+ <%> mempty)
325+
326+ it "API doesn't have these things implemented yet" $
327+ withServantServer api server $ \burl -> do
328+ serverDoesntSatisfy api burl args (getsHaveCacheControlHeader
329+ <%> notAllowedContainsAllowHeader
330+ <%> mempty)
331+```
doc/cookbook/testing/testing.cabalnew file+33
@@ -0,0 +1,33 @@
1+name: cookbook-testing
2+version: 0.0.1
3+synopsis: Common testing patterns in Servant apps
4+description: This recipe includes various strategies for writing tests for Servant.
5+homepage: http://haskell-servant.readthedocs.org/
6+license: BSD3
7+license-file: ../../../servant/LICENSE
8+author: Servant Contributors
9+maintainer: haskell-servant-maintainers@googlegroups.com
10+category: Servant
11+build-type: Simple
12+cabal-version: >=1.10
13+tested-with: GHC==8.0.2, GHC==8.2.2, GHC==8.4.3
14+
15+executable cookbook-testing
16+ main-is: Testing.lhs
17+ build-depends: base == 4.*
18+ , text >= 1.2
19+ , aeson >= 1.2
20+ , servant
21+ , servant-client
22+ , servant-server
23+ , servant-quickcheck
24+ , http-types >= 0.12
25+ , hspec
26+ , hspec-wai
27+ , QuickCheck
28+ , warp >= 3.2
29+ , wai >= 3.2
30+ , wai-extra
31+ default-language: Haskell2010
32+ ghc-options: -Wall -pgmL markdown-unlit
33+ build-tool-depends: markdown-unlit:markdown-unlit