Leveraging NixOS Tests in your Project

By Linus Heckemann, Simon Lackerbauer, Franz Pletz | Thu, 11 Jul 2019

NixOS contains infrastructure for building integration tests based on QEMU/KVM virtual machines running NixOS. Tests built on this infrastructure are continuously run on new nixpkgs versions to ensure that NixOS continues to install and boot and that various services continue to operate correctly. This post illustrates how one may test a simple web service using NixOS tests.

To have a simple enough example at hand, we wrote a small service in PHP—a classical guestbook in which visitors can leave a message that will then be written to a database and subsequently shown to later visitors of the same site. All example code is publicly available on GitHub at mayflower/goldenbook.

Cloning the repo and calling nix-build in the root directory will start a VM with the appropriate services loaded and run the tests provided in test.nix.

Services for Application

Our application is a typical PHP+MySQL application, so the components we will need to be able to run a modern deployment of it are:

  • A web server (we chose nginx)
  • A PHP FastCGI server (we chose php-fpm)
  • A MySQL server (we chose MariaDB)

server.nix is a NixOS configuration module which enables and configures these services. It can be used to deploy the service on production servers by placing it in the server configuration’s imports as well as for testing.

Testing

The principal testing scaffold to run the NixOS tests for our project is provided in default.nix. It applies the nixosTest function from nixpkgs to the file containing the test specification in order to produce a test derivation.

1# default.nix
2{ pkgs ? import <nixpkgs> {} }: {
3  test = pkgs.nixosTest ./test.nix;
4}

The test specification is an attribute set that contains the following attributes:

  • name: the name of the test;
  • nodes: an attribute set containing a NixOS configuration module for each virtual machine used by the test;
  • testScript: a Perl script that runs the actual test.

For instance in our case:

1# test.nix
2{ ... }: {
3  name = "golden-book-test";
4  nodes.server = ./server.nix;
5  testScript = "...";
6}

The Test Script

The testScript parameter for NixOS tests defines how the actual test should run. The script is written in Perl. The machines defined in nodes are exposed as objects which provide a variety of methods for controlling the machines. These range from powering machines on and off to running commands on them to making screenshots and using OCR to get text from them. All these functions are documented in the NixOS manual.

Our test script looks like this:

1startAll;
2# Wait for all services to finish starting
3$server->waitForUnit("multi-user.target");
4# Server should start out with no posts in the database and return an empty but successful response at first
5$server->succeed('[ -z "$(curl --fail localhost)" ]');
6# Making a post should succeed and the server should respond politely
7$server->succeed("curl --fail -F text=testpost localhost | grep Thanks");
8# And after that, the post should be displayed on the main page.
9$server->succeed("curl --fail localhost | grep testpost");

It ensures that the web application functions correctly from a client’s point of view. If any of these commands fail, the test will also fail.

Building the tests using nix-build yields a result directory which contains a report of the test run including logs.

Conclusion

NixOS tests are a powerful tool for integration testing, as they allow sharing configuration between the test environment and production, reducing the gap between the environments and increasing test coverage.

We’ve barely scratched the surface of what the tests allow though! NixOS' own tests include complex tests which for instance simulate bittorrent clients communicating through NATs and with changing network topologies.

Do you have a question about Nix?

Just ask us! We would love to talk to you!

Check out our crew