Base contexts
The sandwich-contexts package provides several useful contexts for adding power to your tests.
This package is mainly concerned with introducing external files and binaries that your tests might need. It also contains some miscellaneous other contexts that don't require any special dependencies.
Other context packages have been split out to address specific needs, such as sandwich-contexts-docker, sandwich-contexts-kubernetes, and , sandwich-contexts-minio.
Nix contexts
The Test.Sandwich.Contexts.Nix module allows you to introduce a NixContext, which represents a snapshot of the Nixpkgs package collection.
You can introduce any version of Nixpkgs that you want. Several are provided in the library, such as nixpkgsRelease2405.
Once you introduce a NixContext
, you can use it to build Nix artifacts. For example, the introduceNixEnvironment function will allow you to build an environment with a given list of packages.
nixDemo :: TopSpec
nixDemo =
introduceNixContext nixpkgsRelease2405 $
introduceNixEnvironment ["emacs", "firefox"] $ do
it "Uses the Nix environment" $ do
env <- getContext nixEnvironment
binaries <- listDirectory (env </> "bin")
info [i|Found binaries in environment: #{binaries}|]
This test will log the various binaries that are available, such as emacs
, emacsclient
, and firefox
. You can now use these artifacts in tests.
File contexts
The Test.Sandwich.Contexts.Files module allows you to introduce named files as contexts. For example, suppose your tests need access to grep
:
tests :: TopSpec
tests =
introduceFile @"grep" "/path/to/grep" $ do
it "uses grep for something" $ do
grep <- askFile @"grep"
results <- readCreateProcess (proc grep ["foo"]) ""
...
Notice the syntax here with the @"grep"
. This is using a combination of type-level strings and -XTypeApplications
to produce some nice readable syntax.
However, hardcoding a path to a file is not very robust. We also have several function for introducing files from the environment. For example, introduceBinaryViaEnvironment will search the PATH for a given binary. This node will fail if the binary is not found, making the failure easy to locate (compared to a more inscrutable error deep in your tests).
tests :: TopSpec
tests =
introduceBinaryViaEnvironment @"grep" $ do
it "uses grep for something" $ do
grep <- askFile @"grep"
...
It's usually a good idea to decouple the "introduce" node from what the tests need. For this reason we provide the HasFile alias, which you can use like this:
filesDemo :: TopSpec
filesDemo =
introduceBinaryViaEnvironment @"grep" $ do
testsWithGrep
testsWithGrep :: (HasFile context "grep") => SpecFree context IO ()
testsWithGrep = do
it "Uses grep binary" $ do
grep <- askFile @"grep"
output <- readCreateProcess (proc grep ["--version"]) ""
info [i|grep --version output: #{output}|]
Now we can wrap testsWithGrep
in any "introduce" node that provides the file! Including more fancy ones, like the Nix-based ones below.
File + Nix contexts = ❤️
Where Nix context and file contexts really shine is when you use them together. For example, the introduceBinaryViaNixPackage function depends on a NixContext being available, and uses it to introduce a given binary from a given package.
You can see that put together here, where we introduce the hello
binary from the hello
package.
spec :: TopSpec
spec = describe "Introducing a Nix binary" $
introduceNixContext nixpkgsReleaseDefault $
introduceBinaryViaNixPackage @"hello" "hello" $ do
it "uses the hello binary" $ do
useHello
useHello :: (MonadIO m, MonadReader context m, HasFile context "hello") => m ()
useHello = do
helloPath <- askFile @"hello"
readCreateProcess (proc helloPath []) "" >>= (`shouldBe` "Hello, world!\n")
This brings together a robust solution for using external binaries:
- It is fully reproducible, as it uses a pinned Nixpkgs checkout.
- The test function
useHello
is decoupled from the introduction method, via theHasFile context "hello"
constraint.
By the way, you can browse all the available Nix packages at search.nixos.org.