Contexts
One of Sandwich's most powerful features is the ability to introduce contexts in tests. A context is simply a labeled dependency, which can be obtained in a test using the getContext
function:
Sandwich gives you the tools to introduce (and gracefully tear down) contexts for use in your tests while keeping the plumbing nicely hidden. The type system enforces that a test has all the contexts it needs.
#
Built-in contextsSandwich provides some contexts automatically. For example, you can retrieve the on-disk folder for a given node by calling getCurrentFolder
. This can be useful if you want to save custom logs, screenshots, etc. to the folder.
Note that getCurrentFolder
returns a Maybe FilePath
. It will be Nothing
if your tests are run without an on-disk folder, or if the particular node in question is configured not to create a folder in its node options.
Another built-in function is getRunRoot
, which will return the root of the on-disk test tree. This can be useful if you want to store test-wide artifacts there. Similar caveats apply when Sandwich is configured to run without on-disk state.
#
Introducing your own contextsSuppose we want to introduce a mock database into some tests. First, we define a label for it. The label represents the mapping between a type-level string and the type of the context. You can find the full working example for this section here.
Next, we write the introduce node. We choose to use introduceWith, because it allows us to use the bracket
pattern to create and then tear down our database. You can imagine IO actions happening here.
Inside the test, we can use getContext
to get the context and do things with it.
#
The HasX pattern for context dependenciesNow let's decouple the introduce node from the test (full working example here). First, we'll define the type of a spec that depends on a database. To do this, we'll need a HasX-style constraint type.
Now, we can use this to define the type of our spec.
Now that we have the spec type, we can start writing specs. You can imagine these living in separate files. These tests don't care about the exact context they're run with, as long as it has a database
available.
Now, in your main test file, you can import both of these tests and run them in the same test tree.
Or, if you want better isolation, you can rearrange this to create a separate database for each subtree.
Either way, the type system ensures that your tests have the contexts they need.
#
Contexts depending on other contextsWe can use the same HasX trick to write contexts that depend on other contexts. For example, suppose you're testing a server and the server depends on a database. You need a database to exist first in order to create the server, and you want both the server and the database available to your tests.
First, let's introduce the Server
type and an introduce function for it. This introduce function is special because it contains a getContext
call to retrieve the database and use it to make the server.
Now, we need to write introduceServer
nested inside a introduceDatabase
node:
Note that it's usually easiest to let GHC infer the type signature of introduceServer
. If you do need to write out the type signature, it can be a little bit verbose since it needs to use the underlying context constructors and put appropriate constraints on the base monad. For this example, the signature for this example might look like this:
The full code for this example can be found here.