This is part of the excellent F# Advent Calendar, if slightly delayed…
One of the joys of working with Suave is the testability that falls out of its composition model. To make those tests really sing, I’ve built a few helpers that wrap some of the underlying type complexity. This in turn allows my tests to be more concise, focused on the behaviour I’m building.
None of this is terribly complex, and that’s part of the fun.
A Brief Introduction to WebParts
Suave’s routing logic is specified by composing WebParts, which are
Briefly, a WebPart takes an
HttpContext as input, does something with
it, and returns an
HttpContext – either
Just a, where
a is whatever the WebPart was intended to do, or
that the WebPart doesn’t care about this particular input. All of this
is wrapped up in an Async workflow, which we don’t need to worry about
What’s important here is that
HttpContext contains all the properties
we care about when testing our routing logic. We’re going to build a
bunch of tools that operate on
HttpContexts, specifically focused on
testing this logic.
For a deeper look at Suave routing, have a look at the docs.
The Very Basics - Requests and Responses
So let’s start by building a request, throwing it at our router, and
verifying that we get a 200 OK out the other side. We’ll fill in some
basic request parameters on
This is perfectly usable as-is, but I’m just noticing that we can separate some concerns by splitting the method and URI parts, and calling them separately:
This shows off our general pattern for building test data: We’re writing
HttpContext, adding info as we need to. We can use the
same approach to add headers, fill in a request body, set cookies, and
so on. For now we’ll proceed with what we have.
Once we’ve done that, we can throw it at a WebPart and see what we get:
(I’m using NUnit and FsUnit, but that’s more or less irrelevant.)
So we threw in an
HttpContext with a well-populated request, and we’d
like to examine the result… but we need to unwrap it first. This part
isn’t as much fun as building a set of combinators, but it’s still
Not as much theoretical fun as chaining combinators, but it does the
job. Note that
expectSome – if we get a
None out of
our router, that thrown exception will have NUnit fail our test.
Notice what we’re not doing: We’re not firing up an OWIN or Nancy instance, installing an ASP.NET site, configuring a DI container so it knows how to build controllers, or any other overhead. All we’re doing is calling a function!
Knowing Where to Look
Let’s try something else. Maybe our GET resource is supposed to return a JSON document with the resource ID in it, and we want to do a simple string match to verify that. We’ll need a way to get the response body, which means destructuring another internal type:
Nothing particularly surprising there. As with
reaching into the
HttpContext for the data we need, this time pulling
out bits of the result. Because the
HttpContext object tracks the
state of the whole web call, it has a lot of structure to it. If you’re
used to dealing with
IHttpActionResults, your standard approach might
be combining educated guesses with autocompletion, but that’s likely to
be less effective here.
A lot of the Suave testing story involves digging into the types in
HttpContext and figuring out what needs to be set (or matched on),
where. For that, you’ll want to keep
open in a browser tab (or maybe an IDE).
A Word About Routers
You’ll notice that I’m calling the router with a single argument (as I
ought to, since
WebParts are defined on a single argument). Obviously,
any meaningful webservice is going to have external dependencies –
persistence stores, monitoring integrations, that sort of thing. I don’t
want to spin those up every time I run a test, so what gives?
Basically, I can define that router as a partial function that takes those dependencies as parameters, and stub in whatever I need for testing. So, something like:
This lets me test failure cases, too:
This is just dependency injection, only we’re doing it without a container. The ease with which F# lets us pass around partially-applied functions makes a DI container unnecessary.
This has been a high-level look at testing Suave’s routing logic.
There’s certainly more to say – we could use
Aether for deeper inspection and
HttpContexts, look at the advantages of testing an
HTTP resource’s routing at the top level versus testing smaller composed
units, and so on – but this is the workflow I’ve stumbled upon and made
work for production code. I hope you’ve found it helpful.
If/when you notice something I can improve, please let me know about it on Twitter.