Skip to content

Testing Utilities

Zig has built-in support for tests.

Zig has built-in support for tests.

A test is declared with the test keyword.

const std = @import("std");

test "addition" {
    try std.testing.expect(1 + 1 == 2);
}

Run the file with:

zig test main.zig

If the condition is true, the test passes.

If the condition is false, the test fails.

std.testing.expect checks a boolean expression.

try std.testing.expect(x == y);

It returns an error when the expression is false, so it is usually called with try.

A test block can contain ordinary Zig code.

const std = @import("std");

fn add(a: i32, b: i32) i32 {
    return a + b;
}

test "add returns the sum" {
    try std.testing.expect(add(3, 4) == 7);
}

Tests can be placed in the same file as the code they test.

This is common in Zig.

A more precise comparison uses expectEqual.

const std = @import("std");

test "equal integers" {
    try std.testing.expectEqual(@as(u32, 42), @as(u32, 42));
}

The first argument is the expected value.

The second argument is the actual value.

try std.testing.expectEqual(expected, actual);

For slices, use expectEqualSlices.

const std = @import("std");

test "equal slices" {
    const a = [_]u8{ 1, 2, 3 };
    const b = [_]u8{ 1, 2, 3 };

    try std.testing.expectEqualSlices(u8, a[0..], b[0..]);
}

The first argument is the element type.

u8

The next two arguments are the slices to compare.

For strings, use expectEqualStrings.

const std = @import("std");

test "equal strings" {
    try std.testing.expectEqualStrings("zig", "zig");
}

Tests can also check for errors.

const std = @import("std");

fn fail() !void {
    return error.BadInput;
}

test "expected error" {
    try std.testing.expectError(error.BadInput, fail());
}

This test passes only if fail() returns error.BadInput.

The testing module also provides an allocator.

const std = @import("std");

test "array list" {
    var list = std.ArrayList(u8).init(std.testing.allocator);
    defer list.deinit();

    try list.append(1);
    try list.append(2);

    try std.testing.expectEqualSlices(u8, &.{ 1, 2 }, list.items);
}

std.testing.allocator is useful because the test runner can detect leaks.

If allocated memory is not freed, the test reports it.

This makes tests good places to check ownership.

A test can import other declarations from the same file.

const std = @import("std");

fn isEven(x: u32) bool {
    return x % 2 == 0;
}

test "even numbers" {
    try std.testing.expect(isEven(2));
    try std.testing.expect(!isEven(3));
}

Tests are declarations.

They can appear beside functions, structs, constants, and variables.

A file may contain many tests.

test "one" {
    try std.testing.expect(true);
}

test "two" {
    try std.testing.expect(10 > 3);
}

All tests in the file are run by zig test.

For larger programs, tests can be run through the build system.

const std = @import("std");

pub fn build(b: *std.Build) void {
    const tests = b.addTest(.{
        .root_module = b.createModule(.{
            .root_source_file = b.path("src/main.zig"),
            .target = b.graph.host,
        }),
    });

    const run_tests = b.addRunArtifact(tests);

    const test_step = b.step("test", "Run tests");
    test_step.dependOn(&run_tests.step);
}

Then run:

zig build test

The test runner builds the test executable and runs it.

A useful pattern is to keep small tests close to the code.

fn clamp(x: i32, lo: i32, hi: i32) i32 {
    if (x < lo) return lo;
    if (x > hi) return hi;
    return x;
}

test "clamp" {
    try std.testing.expectEqual(@as(i32, 0), clamp(-5, 0, 10));
    try std.testing.expectEqual(@as(i32, 5), clamp(5, 0, 10));
    try std.testing.expectEqual(@as(i32, 10), clamp(15, 0, 10));
}

The test describes the contract of the function.

It also provides a small example of use.

Tests should be simple.

They should build values, call functions, and check results.

The best tests make failure easy to locate.

Exercise 14-29. Write a test for a function that returns the larger of two integers.

Exercise 14-30. Write a test that compares two byte slices.

Exercise 14-31. Write a test that expects a specific error.

Exercise 14-32. Use std.testing.allocator in a test and free all allocated memory.