Introduction
Why use nix-unit?
-
Simple structure compatible with
lib.debug.runTests -
Allows individual test attributes to fail individually.
Rather than evaluating the entire test suite in one go, serialise & compare nix-unit uses the Nix evaluator C++ API.
Meaning that we can catch test failures individually, even if the failure is caused by an evaluation error.
- Fast.
No additional processing and coordination overhead caused by the external process approach.
Simple
In it's simplest form a nix-unit test suite is just an attribute set where test attributes are prefix with test.
Test attribute sets contain the keys expr, expressing the test & expected, expressing the expected results.
An expression called test.nix containing:
{
testPass = {
expr = 1;
expected = 1;
};
testFail = {
expr = { x = 1; };
expected = { y = 1; };
};
testFailEval = {
expr = throw "NO U";
expected = 0;
};
}
Evaluated with nix-unit:
$ nix-unit test.nix
Results in the output:
❌ testFail
{ x = 1; } != { y = 1; }
☢️ testFailEval
error:
… while calling the 'throw' builtin
at /home/adisbladis/nix-eval-jobs/test.nix:13:12:
12| testFailEval = {
13| expr = throw "NO U";
| ^
14| expected = 0;
error: NO U
✅ testPass
😢 1/3 successful
error: Tests failed
Flakes
flake.nix
Building on top of the simple classic example the same type of structure could also be expressed in a flake.nix:
{
description = "A very basic flake using nix-unit";
outputs = { self, nixpkgs }: {
libTests = {
testPass = {
expr = 1;
expected = 1;
};
};
};
}
And is evaluated with nix-unit like so:
$ nix-unit --flake '.#libTests'
flake checks
Note: flake-parts can manage this for you.
You can also use nix-unit in flake checks (link).
Create a tests and checks outputs.
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs";
nix-unit.url = "github:nix-community/nix-unit";
nix-unit.inputs.nixpkgs.follows = "nixpkgs";
};
outputs =
{
self,
nixpkgs,
nix-unit,
...
}:
let
forAllSystems = nixpkgs.lib.genAttrs [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"x86_64-windows"
];
in
{
tests.testPass = {
expr = 3;
expected = 4;
};
checks = forAllSystems (system: {
default =
nixpkgs.legacyPackages.${system}.runCommand "tests"
{
nativeBuildInputs = [ nix-unit.packages.${system}.default ];
}
''
export HOME="$(realpath .)"
# The nix derivation must be able to find all used inputs in the nix-store because it cannot download it during buildTime.
nix-unit --eval-store "$HOME" \
--extra-experimental-features flakes \
--override-input nixpkgs ${nixpkgs} \
--flake ${self}#tests
touch $out
'';
});
};
}
Run nix flake check and get an error as expected.
error: builder for '/nix/store/73d58ybnyjql9ddy6lr7fprxijbgb78n-nix-unit-tests.drv' failed with exit code 1;
last 10 log lines:
> /build/nix-20-1/expected.nix --- 1/2 --- Nix
> 1 3
>
> /build/nix-20-1/expected.nix --- 2/2 --- Nix
> 1 4
>
>
>
> 😢 0/1 successful
> error: Tests failed
For full logs, run 'nix log /nix/store/73d58ybnyjql9ddy6lr7fprxijbgb78n-nix-unit-tests.drv'.
Flake-parts
nix-unit provides a flake-parts module for easy integration with flakes.
You can write tests in the option flake.tests and/or perSystem.nix-unit.tests.
The module then takes care of setting up a checks derivation for you.
For this to work, you may have to specify perSystem.nix-unit.inputs to make them available in the derivation.
This tends to require that you flatten some of your inputs tree using follows.
Example
This example can be used with nix flake init -t github:nix-community/nix-unit#flake-parts.
{
description = "Description for the project";
inputs = {
flake-parts.url = "github:hercules-ci/flake-parts";
flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
nix-unit.url = "github:nix-community/nix-unit";
nix-unit.inputs.nixpkgs.follows = "nixpkgs";
nix-unit.inputs.flake-parts.follows = "flake-parts";
};
outputs =
inputs@{ flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
imports = [
inputs.nix-unit.modules.flake.default
];
systems = [
"x86_64-linux"
"aarch64-linux"
"aarch64-darwin"
"x86_64-darwin"
];
perSystem =
{ ... }:
{
nix-unit.inputs = {
# NOTE: a `nixpkgs-lib` follows rule is currently required
inherit (inputs) nixpkgs flake-parts nix-unit;
};
# Tests specified here may refer to system-specific attributes that are
# available in the `perSystem` context
nix-unit.tests = {
"test integer equality is reflexive" = {
expr = "123";
expected = "123";
};
"frobnicator" = {
"testFoo" = {
expr = "foo";
expected = "foo";
};
};
};
};
flake = {
# System-agnostic tests can be defined here, and will be picked up by
# `nix flake check`
tests.testBar = {
expr = "bar";
expected = "bar";
};
};
};
}
Trees
While simple flat attribute sets works you might want to express your tests as a deep attribute set.
When nix-unit encounters an attribute which name is not prefixed with test it recurses into that attribute to find more tests.
Example:
{
testPass = {
expr = 1;
expected = 1;
};
testFail = {
expr = { x = 1; };
expected = { y = 1; };
};
testFailEval = {
expr = throw "NO U";
expected = 0;
};
nested = {
testFoo = {
expr = "bar";
expected = "bar";
};
};
}
Results in the output:
✅ nested.testFoo
❌ testFail
/run/user/1000/nix-244499-0/expected.nix --- Nix
1 { x = 1; } 1 { y = 1; }
☢️ testFailEval
error:
… while calling the 'throw' builtin
at /home/adisbladis/sauce/github.com/nix-community/nix-unit/trees.nix:13:12:
12| testFailEval = {
13| expr = throw "NO U";
| ^
14| expected = 0;
error: NO U
✅ testPass
😢 2/4 successful
error: Tests failed
Testing errors
While testing the happy path is a good start, you might also want to verify that expressions throw the error you expect. You check for a specifc type of error by setting expectedError.type and/or use expectedError.msg to search its message for the given regex.
Example:
tests/default.nix
{
testCatchMessage = {
expr = throw "10 instead of 5";
expectedError.type = "ThrownError";
expectedError.msg = "\\d+ instead of 5";
};
}
->
✅ testCatchMessage
🎉 1/1 successful
Note: Regular expression like the one above are supported
Supported error types
The following values for expectedError.type are valid:
RestrictedPathErrorMissingArgumentErrorUndefinedVarErrorTypeErrorAbortThrownErrorAssertionErrorParseErrorEvalError
coverage
lib.coverage.addCoverage
Generate coverage testing for public interfaces.
public
: The public interface to generate coverage for
tests
: Attribute set of tests to match agains
::: {.example #function-library-example-lib.coverage.addCoverage}
lib.coverage.addCoverage usage example
# Expression
let
# The public interface (attrset) we are testing
public = {
addOne = x: x + 1;
};
# Test suite
tests = {
addOne = {
testAdd = {
expr = public.addOne 1;
expected = 2;
};
};
};
in addCoverage public tests
# Returns
{
addOne = {
testAdd = {
expected = 2;
expr = 2;
};
};
coverage = {
testAddOne = {
expected = true;
expr = true;
};
};
}
:::
Hacking
This document outlines hacking on nix-unit itself.
Getting started
To start hacking run either nix-shell (stable Nix) nix develop (Nix Flakes).
Then create the meson build directory:
$ meson build
$ cd build
And use ninja to build:
$ ninja
Formatter
Before submitting a PR format the code with nix fmt and ensure Flake checks pass with nix flake check.
FAQ
What about a watch mode?
This adds a lot of additional complexity and for now is better dealt with by using external file watcher tools such as Reflex & Watchman.
Can I change the colors?
nix-unit uses difftastic, which can be configured via environment variables. You can turn off
colors via DFT_COLOR=never, give difftastic a hint for choosing better colors with DFT_BACKGROUND=light or see the full
list of options via e.g. nix run nixpkgs#difftastic -- --help.
Comparison with other tools
This comparison matrix was originally taken from Unit test your Nix code but has been adapted. Pythonix is excluded as it's unmaintained.
| Tool | Can test eval failures | Tests defined in Nix | in nixpkgs | snapshot testing(1) |
|---|---|---|---|---|
| Nix-unit | yes | yes | yes | no |
| runTests | no | yes | yes | no |
| Nixt | no | yes | no | no |
| Namaka | no | yes | yes | yes |