| name | creating-nixos-integration-tests |
| description | Creates NixOS VM-based integration tests using testers.runNixOSTest with OpenTelemetry observability. Use when writing isolated system tests for Sway, daemons, multi-machine networking, or any NixOS feature requiring QEMU VM testing with full telemetry to Grafana. |
Creating NixOS Integration Tests
This skill provides guidance for creating NixOS VM-based integration tests with OpenTelemetry observability.
Quick Start
Create a minimal test using pkgs.testers.nixosTest:
# tests/my-feature/default.nix
{ pkgs ? import <nixpkgs> { } }:
pkgs.testers.nixosTest {
name = "my-feature-test";
nodes.machine = { config, pkgs, ... }: {
# NixOS configuration for the test VM
environment.systemPackages = [ pkgs.hello ];
};
testScript = ''
start_all()
machine.wait_for_unit("multi-user.target")
machine.succeed("hello --version")
print("Test passed!")
'';
}
Build and run:
nix-build tests/my-feature -A default
Interactive debugging:
$(nix-build tests/my-feature -A default.driverInteractive)/bin/nixos-test-driver
# Then in Python REPL: start_all(), machine.succeed("cmd"), etc.
Test Structure
A NixOS test consists of three main components:
1. Test Definition
pkgs.testers.nixosTest {
name = "test-name"; # Identifies the test
nodes = { ... }; # VM configurations
testScript = ''...''; # Python test code
}
2. Nodes Configuration
Each node defines a NixOS VM:
nodes.machine = { config, pkgs, ... }: {
# Standard NixOS module configuration
services.nginx.enable = true;
# VM-specific settings
virtualisation.memorySize = 2048;
virtualisation.diskSize = 8192;
};
For multi-machine tests:
nodes = {
server = { ... }: { services.nginx.enable = true; };
client = { ... }: { environment.systemPackages = [ pkgs.curl ]; };
};
3. Test Script
Python code that orchestrates the test:
testScript = ''
start_all() # Boot all VMs in parallel
server.wait_for_unit("nginx.service") # Wait for service
client.succeed("curl -f http://server:80") # Test connectivity
server.screenshot("nginx_running") # Capture state
'';
Machine Methods
Essential methods available on machine objects. See references/machine_methods.md for the complete API.
| Method | Purpose |
|---|---|
start() |
Launch VM asynchronously |
shutdown() |
Graceful power down |
succeed(cmd) |
Run command, assert exit 0 |
fail(cmd) |
Run command, assert non-zero exit |
wait_for_unit(unit) |
Wait for systemd unit to be active |
wait_for_file(path) |
Wait for file to exist |
wait_for_open_port(port) |
Wait for TCP listener |
screenshot(name) |
Capture VM display |
copy_file_from_host(src, dst) |
Transfer file to VM |
Helper functions in testScript:
start_all() # Start all nodes in parallel
# Access unittest assertions via 't':
t.assertIn("expected", output)
OpenTelemetry Integration
Emit test telemetry to Grafana for observability. See references/otel_integration.md for details.
Quick Setup
Include Grafana Alloy in your test VM:
nodes.machine = { config, pkgs, ... }: {
services.grafana-alloy.enable = true;
environment.sessionVariables = {
OTEL_EXPORTER_OTLP_ENDPOINT = "http://localhost:4318";
};
};
Emit Test Spans
Use the provided script to emit test result traces:
scripts/emit_test_span.sh "my-test" "passed" 1500
Or within testScript:
machine.succeed("emit_test_span.sh 'service-check' 'passed' 500")
Templates
Choose a template based on your test requirements:
| Template | Use Case |
|---|---|
| minimal.nix | Basic single-machine tests |
| multi_machine.nix | Network/distributed testing |
| graphical.nix | GUI/Sway testing with VNC |
| otel_traced.nix | Full observability integration |
Copy and customize:
python scripts/init_test.py my-feature --type minimal
Creating a New Test
Follow this workflow when creating tests:
Test Creation Checklist:
- [ ] Step 1: Choose appropriate template
- [ ] Step 2: Define nodes with required NixOS configuration
- [ ] Step 3: Write testScript with assertions
- [ ] Step 4: Build and run interactively to debug
- [ ] Step 5: Add OTEL instrumentation (optional)
- [ ] Step 6: Verify in CI/headless mode
Step 1: Initialize Test File
python scripts/init_test.py my-feature --type minimal
Step 2: Configure VM
Add required NixOS modules and packages:
nodes.machine = { config, pkgs, ... }: {
imports = [ ./my-module.nix ];
services.my-service.enable = true;
environment.systemPackages = [ pkgs.my-tool ];
};
Step 3: Write Test Logic
Use machine methods to verify behavior:
machine.wait_for_unit("my-service.service")
output = machine.succeed("my-tool --status")
assert "running" in output, f"Expected 'running' in: {output}"
Step 4: Debug Interactively
$(nix-build -A default.driverInteractive)/bin/nixos-test-driver
In the Python REPL:
>>> start_all()
>>> machine.succeed("systemctl status my-service")
>>> machine.screenshot("debug")
Step 5: Add Observability
See references/otel_integration.md for detailed patterns.
Common Patterns
Testing Services
# Wait for service to be ready
machine.wait_for_unit("nginx.service")
machine.wait_for_open_port(80)
# Verify service responds correctly
output = machine.succeed("curl -s http://localhost:80")
assert "<html>" in output
Multi-Machine Communication
# Server/client test pattern
server.wait_for_unit("nginx.service")
client.succeed("ping -c 1 server")
client.succeed("curl -f http://server:80")
Testing with Wayland/Sway
See references/graphical_testing.md for VNC and GUI patterns.
machine.wait_for_unit("greetd.service")
machine.wait_for_file("/tmp/sway-ipc.sock")
machine.succeed("swaymsg -t get_version")
Testing Application Launches
See references/app_testing.md for app-launcher patterns.
# Launch app via unified wrapper
machine.succeed("app-launcher-wrapper.sh terminal")
# Verify window appeared
def wait_for_window(app_id, timeout=10):
import time
start = time.time()
while time.time() - start < timeout:
try:
machine.succeed(f"swaymsg -t get_tree | jq '.. | .app_id?' | grep -q {app_id}")
return True
except:
machine.sleep(1)
return False
assert wait_for_window("com.mitchellh.ghostty")
Configuration Options
Virtualization Settings
virtualisation = {
memorySize = 2048; # RAM in MB
diskSize = 8192; # Disk in MB
cores = 2; # CPU cores
graphics = false; # Disable for CI
qemu.options = [ # Custom QEMU flags
"-vga none"
"-device virtio-gpu-pci"
];
};
Headless Wayland (for Sway tests)
environment.sessionVariables = {
WLR_BACKENDS = "headless";
WLR_HEADLESS_OUTPUTS = "3";
WLR_RENDERER = "pixman";
WLR_NO_HARDWARE_CURSORS = "1";
};
Multi-Machine Networking
# Machines on same VLAN can communicate via hostname
nodes = {
server = { virtualisation.vlans = [ 1 ]; };
client = { virtualisation.vlans = [ 1 ]; };
};
testScript = ''
client.succeed("ping -c 1 server")
'';
Debugging Tips
Use interactive mode for rapid iteration:
$(nix-build -A driverInteractive)/bin/nixos-test-driverTake screenshots at failure points:
machine.screenshot("before_failure")Check logs when services fail:
machine.succeed("journalctl -u my-service --no-pager")Clear VM state if corrupted:
rm -rf /tmp/vm-state-machineUse VNC for graphical debugging (see references/graphical_testing.md)
Resources
- references/machine_methods.md - Complete Python API
- references/otel_integration.md - Telemetry patterns
- references/graphical_testing.md - VNC/GUI testing
- references/app_testing.md - Application launcher testing