nixosTest
nixosTest
Goal: Write and run a nixosTest VM test that exercises your module (proxy, hardened SSH, or role from the shared modules chapter)—not only a copy-paste hello world.
Why this day exists
nixos-rebuild on a pet proves little about the next pet. nixosTest boots one or more QEMU VMs from your modules, runs shell Python/Perl test scripts, and fails CI when regressions land. It is the highest leverage test type in NixOS land for system behavior.
Theory 1 — What a nixosTest is
NixOS configs (nodes) → build VM images → boot under QEMU → testScript → pass/fail
| Piece | Role |
|---|---|
nodes |
Attrset of NixOS module stacks |
testScript |
Python (modern) driving VMs |
name |
Test identity |
| Driver | nixos/lib/testing.nix machinery |
Requires KVM (or slow emulation) and often builder feature kvm / nixos-test.
Theory 2 — Minimal single-node test
# tests/ssh-hardening.nix
{
name = "ssh-hardening";
nodes.machine = { pkgs, ... }: {
imports = [ ../modules/profiles/hardened.nix ];
users.users.alice = {
isNormalUser = true;
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExampleKeyForTestOnly not-a-real-key"
];
};
services.openssh.enable = true;
};
testScript = ''
machine.wait_for_unit("sshd.service")
machine.succeed("grep -q 'PasswordAuthentication no' /etc/ssh/sshd_config")
# or sshd -T output depending on version
machine.succeed("sshd -T | grep -i 'passwordauthentication no'")
'';
}Wire into flake:
checks.x86_64-linux.ssh-hardening =
nixpkgs.lib.nixos.runTest {
imports = [ ./tests/ssh-hardening.nix ];
hostPkgs = import nixpkgs { system = "x86_64-linux"; };
# API: prefer current nixpkgs `runTest` / `nixosTest` helper for 26.05
};nixpkgs has migrated helpers (pkgs.testers.runNixOSTest, lib.nixos.runTest, classic nixosTest). Use whatever your 26.05 pin documents; the shape (nodes + testScript) remains the skill.
Classic style still seen in blogs:
import nixpkgs {
system = "x86_64-linux";
config = { allowUnfree = false; };
} .lib.nixosSystem { } # not this
# classic:
pkgs.nixosTest {
name = "example";
nodes.machine = { ... };
testScript = ''...'';
}Theory 3 — testScript essentials
machine.wait_for_unit("multi-user.target")
machine.wait_for_open_port(22)
machine.succeed("true")
machine.fail("false")
machine.copy_from_host("file", "/tmp/file")
print(machine.succeed("uname -a"))Multi-node:
nodes = {
server = { ... };
client = { ... };
};server.wait_for_unit("caddy.service")
client.succeed("curl -f http://server/")Network is usually a test VLAN; hostnames often equal node names.
Theory 4 — What to test (high value)
| Good | Weak |
|---|---|
| Module assertions about sshd config | true only |
| Service reaches active + port open | Only that evaluation succeeds |
| Client→server HTTP 200 | Full browser suites |
| Firewall rejects unexpected port | Entire Kubernetes |
Test your contracts from the hardening chapter–38 / 61.
Theory 5 — Running tests
nix build .#checks.x86_64-linux.ssh-hardening -L
# or
nix flake check -LInteractive debug (when supported by driver):
nix build .#checks… -L
# many setups: ./result/bin/nixos-test-driver with breakpoints in testScriptmachine.shell_interact() # if available in your nixpkgs test driverWorked example — web role smoke test
# tests/web-role.nix
{
name = "web-role";
nodes.server = { ... }: {
imports = [
../modules/profiles/baseline.nix
../modules/roles/web.nix
];
roles.web = {
enable = true;
domain = "server"; # or localhost style per caddy config
};
};
nodes.client = { pkgs, ... }: {
environment.systemPackages = [ pkgs.curl ];
};
testScript = ''
server.wait_for_unit("caddy.service")
server.wait_for_open_port(80)
client.succeed("curl -f --retry 5 --retry-delay 1 http://server/ | grep -q ok")
'';
}Adapt to your actual reverse proxy module from Stage IV.
Lab — multi-step
Suggested workspace: fleet flake tests/; notes in ~/lab/90daysofx/02-nixos/day64
Step 1 — Confirm virtualization
ls /dev/kvm && echo kvm-ok || echo "need kvm or nested virt"Step 2 — Hello test
Minimal machine with environment.etc."t".text = "ok"; and succeed("test -f /etc/t").
Step 3 — Port your module
SSH hardening or web role or custom Stage IV module.
Step 4 — Make it fail once
Break the module; observe red test; fix.
Step 5 — Wire checks
nix flake check runs it (may be heavy—ok).
Step 6 — Document runtime
Wall clock; note CI machine requirements (the CI pipelines chapter).
Step 7 — Multi-node stretch
Client/server only if single-node is green.
Theory 6 — Tests as flake checks
checks.${system}.lab-ssh = self.nixosConfigurations.lab.config.system.build.toplevel; # heavy; optional
# better: dedicated nixosTest derivation
checks.${system}.web-smoke = pkgs.nixosTest { … };Wiring tests into checks makes nix flake check the Stage VI/CI gate. Keep at least one fast smoke and quarantine long tests behind explicit attrnames.
Theory 7 — Interactive vs pure test runs
nix build .#checks.x86_64-linux.web-smoke -L
# driver interactively (when supported by your setup):
# nix run .#checks.…driverInteractiveInteractive drivers help author tests; CI should run non-interactive builds.
Worked example — wait for unit + port
machine.wait_for_unit("caddy.service")
machine.wait_for_open_port(443)
machine.succeed("curl -k -sS https://127.0.0.1 | grep -q lab")Prefer wait_for_unit / wait_for_open_port over sleep 30.
Lab — one negative assertion
machine.fail("curl -sS --max-time 2 http://127.0.0.1:5432 || true")
# better: assert postgres not on 0.0.0.0 if that is policyEncode a security/policy property, not only happy path.
Lab — time the test
time nix build .#checks.x86_64-linux.web-smoke -LRecord duration. Tests that take 20+ minutes will not be run and thus do not protect you—split or slim.
Common gotchas
| Symptom / mistake | What to do |
|---|---|
No /dev/kvm |
Nested virt; remote builder with kvm; or accept qemu slow |
| Hang on boot | Serial logs; simplify modules; wait_for_unit |
| DNS names | Use node names as hostnames |
| Testing wrong module version | Same flake inputs as deploy |
| Over-large configs | Slim test nodes; don’t import Desktop |
| Flaky timing | wait_for_open_port, retries |
Checkpoint
- One nixosTest green
- Tests your module behavior
- Intentional red→green cycle
- Flake
checkswiring
- KVM/runtime notes written
Commit
git add .
git commit -m "day64: nixosTest for own module"Write three personal gotchas before continuing.