2
votes

I'm building an API with Servant, and it seems to work pretty well. And in line with best practices I am writing some tests for it, following the official guide here. But I'm struggling with the part using Servant-Quickcheck.

I'm trying to use it to test that my API doesn't give any 500 errors, like this:

quickCheckSpec :: Spec
quickCheckSpec = describe "QuickCheck global tests for public API -" $ do
  it "API never gives 500 error" $
    withServantServer publicAPI (return server) $ \burl ->
      serverSatisfies publicAPI burl args (not500 <%> mempty)

but unfortunately the test is failing. This is a surprise to me, given that I've not encountered such an error when testing manually - but hey, this is why we have automated tests, right? Especially with tools like QuickCheck which, as far as I understand, is supposed to zero in on "edge cases" that you might not think to test manually. (This is actually my first time using QuickCheck in any form.)

The problem is though that a test failure is useless unless you know what it failed on. And this is what Servant-QuickCheck does not seem to want to tell me. The only output I get regarding the test failure is this:

  API never gives 500 error FAILED [1]

Failures:

  src\\Servant\\QuickCheck\\Internal\\QuickCheck.hs:146:11: 
  1) QuickCheck global tests for public API - API never gives 500 error
       Nothing

  To rerun use: --match "/QuickCheck global tests for public API -/API never gives 500 error/"

Randomized with seed 1712537046

Finished in 10.8513 seconds
4 examples, 1 failure

(There are some unit tests run before, which all pass - the 10 seconds isn't all on the above test!)

This is rather bemusing, because as you can see there's nothing which indicates how the failure occurred. Since it's testing an API and, as far as I understand it, choosing valid routes at random to test with, it ought to tell me at which route it encountered the 500 error. Yet, as you can see, there is precisely nothing there.

(I'm developing the project with Stack btw, and a little bit below the above it says Logs printed to console, so I don't believe I'm missing any log files buried somewhere which could enlighten me. I haven't been able to find any in my .stack-work folder either.)

I've even changed my args so that it has the chatty field set to True, but I still only get the above.

The only clue I get, which isn't related to QuickCheck, is that my API uses Persistent and logs the database queries to the terminal, so I can see the queries that have been run - which I can in turn relate back to the route. However I don't particularly trust this, because the route that runs the query shown is definitely working in manual tests. (And it's not always the same query when it fails.) There are also a couple of simple routes that don't query the database, so the failure isn't necessarily related to the last database query printed (although needless to say, I've manually tested those routes too and they're fine).

I've even wondered if the problem might not be that QuickCheck hits my localhost server so many times in a row that it simply can't cope and errors out, but in that case I would expect this to be a common problem, remarked upon in the tutorial. (And the database logging output suggests that it in fact always fails on the very first "hit".)

Anyway, that's just speculation, and I realise that it's up to me to figure out why my API might be failing the tests, given that I didn't think it relevant to share any details of my actual API.

What I'm really asking is: why am I not told which routes were tested, and which ones passed/failed, so that I at least know exactly which route(s) to investigate? And is there any way to make Servant-Quickcheck show me that information?

Any answer to that would be much appreciated.

FURTHER INFORMATION: I've tried with @MarkusBarthlen's suggestion below, but it didn't give me any more information. However, it did point up that the Nothing which you can see in my console output is probably somehow key to this, because in Markus's example he gets a Just value holding the request information. Looking at line 146 of the source file referenced in the failure message, it's clearly the x there that apparently is a Maybe value. But I can't figure out what type it actually is, and what the significance is of it being Nothing in my case. Unfortunately the code is beyond my own Haskell experience/ability to make much sense of - I can see the x is read out of an MVar, which is populated somehow with the result of the test, but as I said I'm not able to make sense of the details. But I really need to know, not only why my test is failing, but why this particular x value is Nothing for me, and what that means in terms of the test. I would really appreciate an answer that explains this in a way I can understand.

1

1 Answers

0
votes

From what I see, you could write a RequestPredicate as in https://github.com/haskell-servant/servant-quickcheck/blob/master/src/Servant/QuickCheck/Internal/Predicates.hs for example notLongerThan.

I tested it with

not200b :: RequestPredicate
not200b = RequestPredicate $ \req mgr -> do
     resp <- httpLbs req mgr
     when (responseStatus resp == status200) $ throw $ PredicateFailure "not200b" (Just req) resp
     return []

yielding a response of

Failures:

src/Servant/QuickCheck/Internal/QuickCheck.hs:146:11: 1) QuickCheck global tests for public API - API never gives 200 Failed: Just Predicate failed Predicate: not200b Request: Method: "GET" Path: /users Headers: Body: Response: Status code: 200 Headers: "Transfer-Encoding": "chunked" "Date": "Fri, 22 Nov 2019 21:32:07 GMT" "Server": "Warp/3.2.28" "Content-Type": "application/json;charset=utf-8" Body: [{"userId":1,"userFirstName":"Isaac","userLastName":"Newton"},{"userId":2,"userFirstName":"Albert","userLastName":"Einstein"}]

To rerun use: --match "/QuickCheck global tests for public API -/API never gives 200/"

Randomized with seed 1026627332

Finished in 0.0226 seconds 1 example, 1 failure