In addition to Unit and Integration tests, you can also write tests that interact with your application as a real user would. That technique is called End to End(E2E) testing.

In this example we have a workspace with two members:

  • server: a web server that uses Axum for HTTP and Sqlx connect to an instance of PostgreSQL
  • e2e: a end-to-end test "script" that drives Firefox into interacting with the sever

Quick-start an E2E project in a fresh directory with:

nix flake init -t github:ipetkov/crane#end-to-end-testing

Alternatively, if you have an existing project already, copy and paste the following flake.nix and modify it to build your workspace's packages:

{
  description = "Example E2E testing";
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";

    crane = {
      url = "github:ipetkov/crane";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    flake-utils.url = "github:numtide/flake-utils";

    rust-overlay = {
      url = "github:oxalica/rust-overlay";
      inputs = {
        nixpkgs.follows = "nixpkgs";
        flake-utils.follows = "flake-utils";
      };
    };
  };
  outputs = { nixpkgs, crane, flake-utils, rust-overlay, ... }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = import nixpkgs {
          inherit system;
          overlays = [ (import rust-overlay) ];
        };
        inherit (pkgs) lib;

        rustToolchain = pkgs.rust-bin.stable.latest.default;
        craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain;
        src = craneLib.cleanCargoSource (craneLib.path ./.);

        workspace = craneLib.buildPackage {
          inherit src;
          pname = "example-e2e";
          version = "0.1";
          doCheck = false;
          nativeBuildInputs = lib.optionals pkgs.stdenv.isDarwin
            (with pkgs.darwin.apple_sdk.frameworks; [
              pkgs.libiconv
              CoreFoundation
              Security
              SystemConfiguration
            ]);
        };

        # The script inlined for brevity, consider extracting it
        # so that it becomes independent of nix
        runE2ETests = pkgs.runCommand "e2e-tests"
          {
            nativeBuildInputs = with pkgs; [
              retry
              curl
              geckodriver
              firefox
              cacert
              postgresql
            ];
          } ''

          wait-for-connection() {
            timeout 5s \
              retry --until=success --delay "1" -- \
                curl -s "$@"
          }

          initdb postgres-data
          pg_ctl --pgdata=postgres-data --options "-c unix_socket_directories=$PWD" start
          export DATABASE_URL="postgres:///postgres?host=$PWD"
          psql "$DATABASE_URL" <<EOF
            CREATE TABLE users(name TEXT);
          EOF

          ${workspace}/bin/server &
          wait-for-connection --fail localhost:8000

          # Firefox likes to write to $HOME
          HOME="$(mktemp -d)" geckodriver &
          wait-for-connection localhost:4444

          ${workspace}/bin/e2e_tests

          touch $out
        '';

        pkgsSupportsPackage = pkg:
          (lib.elem system pkg.meta.platforms) && !(lib.elem system pkg.meta.badPlatforms);
      in
      {
        checks = {
          inherit workspace;
          # Firefox is broken in some platforms (namely "aarch64-apple-darwin"), skip those
        } // (lib.optionalAttrs (pkgsSupportsPackage pkgs.firefox) {
          inherit runE2ETests;
        });

        devShells.default = pkgs.mkShell {
          BuildInputs = with pkgs; [
            rustc
            cargo
          ] ++ (lib.optionals (!pkgs.stdenv.isDarwin) [
            geckodriver
            firefox
          ]);
        };
      });
}