Use a different server-running method and add bulleted list of strategies at start
This commit is contained in:
parent
0d0fd0de82
commit
64f89f600a
1 changed files with 73 additions and 28 deletions
|
@ -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).
|
Loading…
Reference in a new issue