1# Writing Tests {#sec-writing-nixos-tests} 2 3A NixOS test is a module that has the following structure: 4 5```nix 6{ 7 8 # One or more machines: 9 nodes = 10 { machine = 11 { config, pkgs, ... }: { }; 12 machine2 = 13 { config, pkgs, ... }: { }; 14 15 }; 16 17 testScript = 18 '' 19 Python code… 20 ''; 21} 22``` 23 24We refer to the whole test above as a test module, whereas the values 25in [`nodes.<name>`](#test-opt-nodes) are NixOS modules themselves. 26 27The option [`testScript`](#test-opt-testScript) is a piece of Python code that executes the 28test (described below). During the test, it will start one or more 29virtual machines, the configuration of which is described by 30the option [`nodes`](#test-opt-nodes). 31 32An example of a single-node test is 33[`login.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/login.nix). 34It only needs a single machine to test whether users can log in 35on the virtual console, whether device ownership is correctly maintained 36when switching between consoles, and so on. An interesting multi-node test is 37[`nfs/simple.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/nfs/simple.nix). 38It uses two client nodes to test correct locking across server crashes. 39 40## Calling a test {#sec-calling-nixos-tests} 41 42Tests are invoked differently depending on whether the test is part of NixOS or lives in a different project. 43 44### Testing within NixOS {#sec-call-nixos-test-in-nixos} 45 46Tests that are part of NixOS are added to [`nixos/tests/all-tests.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/all-tests.nix). 47 48```nix 49 hostname = runTest ./hostname.nix; 50``` 51 52Overrides can be added by defining an anonymous module in `all-tests.nix`. 53 54```nix 55 hostname = runTest { 56 imports = [ ./hostname.nix ]; 57 defaults.networking.firewall.enable = false; 58 }; 59``` 60 61You can run a test with attribute name `hostname` in `nixos/tests/all-tests.nix` by invoking: 62 63```shell 64cd /my/git/clone/of/nixpkgs 65nix-build -A nixosTests.hostname 66``` 67 68### Testing outside the NixOS project {#sec-call-nixos-test-outside-nixos} 69 70Outside the `nixpkgs` repository, you can instantiate the test by first importing the NixOS library, 71 72```nix 73let nixos-lib = import (nixpkgs + "/nixos/lib") { }; 74in 75 76nixos-lib.runTest { 77 imports = [ ./test.nix ]; 78 hostPkgs = pkgs; # the Nixpkgs package set used outside the VMs 79 defaults.services.foo.package = mypkg; 80} 81``` 82 83`runTest` returns a derivation that runs the test. 84 85## Configuring the nodes {#sec-nixos-test-nodes} 86 87There are a few special NixOS options for test VMs: 88 89`virtualisation.memorySize` 90 91: The memory of the VM in megabytes. 92 93`virtualisation.vlans` 94 95: The virtual networks to which the VM is connected. See 96 [`nat.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/nat.nix) 97 for an example. 98 99`virtualisation.writableStore` 100 101: By default, the Nix store in the VM is not writable. If you enable 102 this option, a writable union file system is mounted on top of the 103 Nix store to make it appear writable. This is necessary for tests 104 that run Nix operations that modify the store. 105 106For more options, see the module 107[`qemu-vm.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/virtualisation/qemu-vm.nix). 108 109The test script is a sequence of Python statements that perform various 110actions, such as starting VMs, executing commands in the VMs, and so on. 111Each virtual machine is represented as an object stored in the variable 112`name` if this is also the identifier of the machine in the declarative 113config. If you specified a node `nodes.machine`, the following example starts the 114machine, waits until it has finished booting, then executes a command 115and checks that the output is more-or-less correct: 116 117```py 118machine.start() 119machine.wait_for_unit("default.target") 120if not "Linux" in machine.succeed("uname"): 121 raise Exception("Wrong OS") 122``` 123 124The first line is technically unnecessary; machines are implicitly started 125when you first execute an action on them (such as `wait_for_unit` or 126`succeed`). If you have multiple machines, you can speed up the test by 127starting them in parallel: 128 129```py 130start_all() 131``` 132 133If the hostname of a node contains characters that can't be used in a 134Python variable name, those characters will be replaced with 135underscores in the variable name, so `nodes.machine-a` will be exposed 136to Python as `machine_a`. 137 138## Machine objects {#ssec-machine-objects} 139 140The following methods are available on machine objects: 141 142@PYTHON_MACHINE_METHODS@ 143 144To test user units declared by `systemd.user.services` the optional 145`user` argument can be used: 146 147```py 148machine.start() 149machine.wait_for_x() 150machine.wait_for_unit("xautolock.service", "x-session-user") 151``` 152 153This applies to `systemctl`, `get_unit_info`, `wait_for_unit`, 154`start_job` and `stop_job`. 155 156For faster dev cycles it's also possible to disable the code-linters 157(this shouldn't be committed though): 158 159```nix 160{ 161 skipLint = true; 162 nodes.machine = 163 { config, pkgs, ... }: 164 { configuration 165 }; 166 167 testScript = 168 '' 169 Python code… 170 ''; 171} 172``` 173 174This will produce a Nix warning at evaluation time. To fully disable the 175linter, wrap the test script in comment directives to disable the Black 176linter directly (again, don't commit this within the Nixpkgs 177repository): 178 179```nix 180 testScript = 181 '' 182 # fmt: off 183 Python code… 184 # fmt: on 185 ''; 186``` 187 188Similarly, the type checking of test scripts can be disabled in the following 189way: 190 191```nix 192{ 193 skipTypeCheck = true; 194 nodes.machine = 195 { config, pkgs, ... }: 196 { configuration 197 }; 198} 199``` 200 201## Failing tests early {#ssec-failing-tests-early} 202 203To fail tests early when certain invariants are no longer met (instead of waiting for the build to time out), the decorator `polling_condition` is provided. For example, if we are testing a program `foo` that should not quit after being started, we might write the following: 204 205```py 206@polling_condition 207def foo_running(): 208 machine.succeed("pgrep -x foo") 209 210 211machine.succeed("foo --start") 212machine.wait_until_succeeds("pgrep -x foo") 213 214with foo_running: 215 ... # Put `foo` through its paces 216``` 217 218`polling_condition` takes the following (optional) arguments: 219 220`seconds_interval` 221 222: specifies how often the condition should be polled: 223 224```py 225@polling_condition(seconds_interval=10) 226def foo_running(): 227 machine.succeed("pgrep -x foo") 228``` 229 230`description` 231 232: is used in the log when the condition is checked. If this is not provided, the description is pulled from the docstring of the function. These two are therefore equivalent: 233 234```py 235@polling_condition 236def foo_running(): 237 "check that foo is running" 238 machine.succeed("pgrep -x foo") 239``` 240 241```py 242@polling_condition(description="check that foo is running") 243def foo_running(): 244 machine.succeed("pgrep -x foo") 245``` 246 247## Adding Python packages to the test script {#ssec-python-packages-in-test-script} 248 249When additional Python libraries are required in the test script, they can be 250added using the parameter `extraPythonPackages`. For example, you could add 251`numpy` like this: 252 253```nix 254{ 255 extraPythonPackages = p: [ p.numpy ]; 256 257 nodes = { }; 258 259 # Type checking on extra packages doesn't work yet 260 skipTypeCheck = true; 261 262 testScript = '' 263 import numpy as np 264 assert str(np.zeros(4) == "array([0., 0., 0., 0.])") 265 ''; 266} 267``` 268 269In that case, `numpy` is chosen from the generic `python3Packages`. 270 271## Test Options Reference {#sec-test-options-reference} 272 273The following options can be used when writing tests. 274 275```{=include=} options 276id-prefix: test-opt- 277list-id: test-options-list 278source: @NIXOS_TEST_OPTIONS_JSON@ 279```