⚡ Zig Guide LiveUnofficialbut fully verified
✓ Zig 0.17.0-dev.2122+3e15e99e6What's newOn an older Zig?

Running Tests

A test block is a language construct, not a framework. No imports, no registration, no annotations.

const std = @import("std");
const expect = std.testing.expect;

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

test "a test is just a named block" {
    try expect(add(2, 2) == 4);
}

test "expectEqual reports both values on failure" {
    try std.testing.expectEqual(@as(i32, 4), add(2, 2));
}

test "comparing slices" {
    try std.testing.expectEqualStrings("abc", "ab" ++ "c");
    try std.testing.expectEqualSlices(u8, &[_]u8{ 1, 2 }, &[_]u8{ 1, 2 });
}

test "asserting an error is returned" {
    const failing = struct {
        fn f() error{Nope}!void {
            return error.Nope;
        }
    }.f;
    try std.testing.expectError(error.Nope, failing());
}

test "the testing allocator fails the test on a leak" {
    const gpa = std.testing.allocator;
    const buf = try gpa.alloc(u8, 10);
    defer gpa.free(buf); // remove this line and the test fails
    try expect(buf.len == 10);
}

test "skipping" {
    if (@import("builtin").cpu.arch != .wasm32) return error.SkipZigTest;
    try expect(true);
}

Running them

zig test file.zig        # just this file
zig build test           # the test step of a build.zig
zig test file.zig --test-filter "name"

Tests in a file only run if that file is the test root or is reachable from it via @import. A test in a module nobody imports never runs, which is worth knowing when a test you expected to fail stays quiet.

The assertion helpers

HelperUse
expect(bool)general condition
expectEqual(a, b)prints both values on failure
expectEqualStringsstring comparison with a readable diff
expectEqualSlices(T, a, b)slice comparison
expectError(err, expr)asserts a specific error

Prefer expectEqual over expect(a == b): when it fails, it tells you what the values actually were.

Leak detection is on by default

std.testing.allocator fails the test if anything it allocated was not freed. This is not an optional tool you remember to run. Every test that allocates is a leak test:

const buf = try std.testing.allocator.alloc(u8, 10);
defer std.testing.allocator.free(buf);   // omit this and the test fails

Skipping

return error.SkipZigTest marks a test skipped rather than failed. It is the way to handle a test that only applies to some targets.

Where to put them

Next to the code they test, in the same file. That is the convention in the standard library and it is worth following, for two reasons that are easy to miss.

The test can reach private declarations, because privacy is per file. A helper that is not pub has no other way to be tested. Moving tests to a separate file pushes you toward making things public that should not be.

And the test is read as the example. Someone opening the file to work out how a function is called finds a call, with real arguments and a checked result, a few lines below the definition. Documentation in a separate directory does not get read that way.

Tests are not compiled into your program. A test block only exists in a build produced by zig test or a test step. So there is no cost to leaving them in place, and no reason to move them at release time.

Naming them

The name is a plain string, so write a sentence:

test "parseInt rejects a trailing space" { ... }

That name is what the runner prints on failure and what --test-filter matches, so a description of the behaviour is more useful than a label. Reading a failing test’s name and knowing what broke, without opening the file, is the whole benefit.

Reading a failure

A failed assertion prints the values, then a stack trace pointing at the line. The trace is the useful part and it is only there because tests are built in Debug by default, with safety checks on. That is also why a test can catch an out-of-bounds index that a release build would let through.

These pages are the tests

Every snippet in this guide is compiled and run by CI, and the test ones report through this same runner. The output you see when you press Run is the real zig test output, executed in your browser as WebAssembly.

There is more in testing. The assertions that report what actually differed. Checking that error paths clean up after themselves. And why the testing allocator turns every test into a leak test.