Skip to main content

Testing Framework

Zig includes a comprehensive testing framework built into the standard library. Tests are first-class citizens in Zig - they’re part of the language itself, not an external tool.

Writing Tests

Tests are declared using the test keyword:
const std = @import("std");
const testing = std.testing;

test "basic addition" {
    try testing.expectEqual(4, 2 + 2);
}

test "string comparison" {
    try testing.expectEqualStrings("hello", "hello");
}
Run tests with:
zig test myfile.zig
Test blocks are only compiled when running zig test. They’re completely omitted from regular builds, so there’s zero runtime overhead.

Testing Allocator

The testing module provides a special allocator that detects memory leaks:
lib/std/testing.zig:10-29
pub const FailingAllocator = @import("testing/FailingAllocator.zig");
pub const failing_allocator = failing_allocator_instance.allocator();

/// This should only be used in temporary test programs.
pub const allocator = allocator_instance.allocator();
pub var allocator_instance: std.heap.GeneralPurposeAllocator(.{
    .stack_trace_frames = if (std.debug.sys_can_stack_trace) 10 else 0,
    .resize_stack_traces = true,
    // A unique value so that when a default-constructed
    // GeneralPurposeAllocator is incorrectly passed to testing allocator, or
    // vice versa, panic occurs.
    .canary = @truncate(0x2731e675c3a701ba),
}) = b: {
    if (!builtin.is_test) @compileError("testing allocator used when not testing");
    break :b .init;
};
Usage:
const std = @import("std");
const testing = std.testing;

test "allocator detects leaks" {
    const allocator = testing.allocator;
    
    const memory = try allocator.alloc(u8, 100);
    defer allocator.free(memory); // Must free or test fails
    
    // Use memory...
}
Always use testing.allocator in tests. It will automatically detect leaks and fail the test if memory isn’t freed properly.

Assertion Functions

The std.testing module provides extensive assertion functions:

expectEqual

Compare two values for equality:
lib/std/testing.zig:72-79
/// This function is intended to be used only in tests. When the two values are not
/// equal, prints diagnostics to stderr to show exactly how they are not equal,
/// then returns a test failure error.
/// `actual` and `expected` are coerced to a common type using peer type resolution.
pub inline fn expectEqual(expected: anytype, actual: anytype) !void {
    const T = @TypeOf(expected, actual);
    return expectEqualInner(T, expected, actual);
}
test "expectEqual examples" {
    try testing.expectEqual(42, 40 + 2);
    try testing.expectEqual(@as(i32, -1), -1);
    try testing.expectEqual(true, 1 == 1);
}

expectEqualSlices

Compare two slices element by element:
lib/std/testing.zig:355-374
/// This function is intended to be used only in tests. When the two slices are not
/// equal, prints diagnostics to stderr to show exactly how they are not equal (with
/// the differences highlighted in red), then returns a test failure error.
/// If your inputs are UTF-8 encoded strings, consider calling `expectEqualStrings` instead.
pub fn expectEqualSlices(comptime T: type, expected: []const T, actual: []const T) !void {
    const diff_index: usize = diff_index: {
        const shortest = @min(expected.len, actual.len);
        var index: usize = 0;
        while (index < shortest) : (index += 1) {
            if (!std.meta.eql(actual[index], expected[index])) break :diff_index index;
        }
        break :diff_index if (expected.len == actual.len) return else shortest;
    };
    // Print detailed diagnostics...
}
test "slice comparison" {
    const expected = [_]i32{ 1, 2, 3, 4 };
    const actual = [_]i32{ 1, 2, 3, 4 };
    try testing.expectEqualSlices(i32, &expected, &actual);
}
When slices differ, expectEqualSlices prints a detailed diff showing exactly where they diverge, with differences highlighted in color.

expectEqualStrings

Specialized string comparison with UTF-8 awareness:
test "string equality" {
    try testing.expectEqualStrings("hello, world", "hello, world");
    
    const str1 = "Zig is fast";
    const str2 = "Zig is fast";
    try testing.expectEqualStrings(str1, str2);
}

expectError

Verify that an expression returns a specific error:
lib/std/testing.zig:55-70
/// This function is intended to be used only in tests. It prints diagnostics to stderr
/// and then returns a test failure error when actual_error_union is not expected_error.
pub fn expectError(expected_error: anyerror, actual_error_union: anytype) !void {
    if (actual_error_union) |actual_payload| {
        print("expected error.{s}, found {any}\n", .{ @errorName(expected_error), actual_payload });
        return error.TestExpectedError;
    } else |actual_error| {
        if (expected_error != actual_error) {
            print("expected error.{s}, found error.{s}\n", .{
                @errorName(expected_error),
                @errorName(actual_error),
            });
            return error.TestUnexpectedError;
        }
    }
}
const std = @import("std");
const testing = std.testing;

fn parseNumber(str: []const u8) !i32 {
    if (str.len == 0) return error.EmptyString;
    return 42; // Simplified
}

test "error handling" {
    try testing.expectError(error.EmptyString, parseNumber(""));
}

expect

Simple boolean assertion:
test "boolean assertions" {
    try testing.expect(true);
    try testing.expect(5 > 3);
    try testing.expect("hello".len == 5);
}

expectApproxEqAbs / expectApproxEqRel

Floating-point comparison with tolerance:
lib/std/testing.zig:282-314
/// This function is intended to be used only in tests. When the actual value is
/// not approximately equal to the expected value, prints diagnostics to stderr
/// to show exactly how they are not equal, then returns a test failure error.
/// See `math.approxEqAbs` for more information on the tolerance parameter.
pub inline fn expectApproxEqAbs(expected: anytype, actual: anytype, tolerance: anytype) !void {
    const T = @TypeOf(expected, actual, tolerance);
    return expectApproxEqAbsInner(T, expected, actual, tolerance);
}

/// See `math.approxEqRel` for more information on the tolerance parameter.
pub inline fn expectApproxEqRel(expected: anytype, actual: anytype, tolerance: anytype) !void {
    const T = @TypeOf(expected, actual, tolerance);
    return expectApproxEqRelInner(T, expected, actual, tolerance);
}
test "floating point comparison" {
    const a: f32 = 0.1 + 0.2;
    const b: f32 = 0.3;
    
    // Absolute tolerance
    try testing.expectApproxEqAbs(b, a, 0.0001);
    
    // Relative tolerance  
    try testing.expectApproxEqRel(b, a, 0.01);
}

expectFmt

Compare formatted output:
lib/std/testing.zig:266-278
/// This function is intended to be used only in tests. When the formatted result of the template
/// and its arguments does not equal the expected text, it prints diagnostics to stderr to show how
/// they are not equal, then returns an error.
pub fn expectFmt(expected: []const u8, comptime template: []const u8, args: anytype) !void {
    if (@inComptime()) {
        var buffer: [std.fmt.count(template, args)]u8 = undefined;
        return expectEqualStrings(expected, try std.fmt.bufPrint(&buffer, template, args));
    }
    const actual = try std.fmt.allocPrint(allocator, template, args);
    defer allocator.free(actual);
    return expectEqualStrings(expected, actual);
}
test "formatted output" {
    try testing.expectFmt("Value: 42", "Value: {}", .{42});
    try testing.expectFmt("Hello, Zig!", "Hello, {}!", .{"Zig"});
}

Test Organization

Test Namespaces

Organize related tests in containers:
const std = @import("std");
const testing = std.testing;

const math = struct {
    pub fn add(a: i32, b: i32) i32 {
        return a + b;
    }
    
    pub fn multiply(a: i32, b: i32) i32 {
        return a * b;
    }
};

test "math.add" {
    try testing.expectEqual(5, math.add(2, 3));
}

test "math.multiply" {
    try testing.expectEqual(6, math.multiply(2, 3));
}

Referencing All Declarations

Ensure all public declarations are tested:
lib/std/std.zig:184-186
test {
    testing.refAllDecls(@This());
}
const std = @import("std");

pub const MyStruct = struct {
    value: i32,
    
    test {
        std.testing.refAllDecls(@This());
    }
};

test {
    std.testing.refAllDecls(@This());
}
refAllDecls creates references to all public declarations, ensuring they get compiled and any associated tests are run.

Testing Best Practices

1. Test Names Should Be Descriptive

// Good
test "ArrayList.append increases length by one" {
    // ...
}

// Less good
test "append" {
    // ...
}

2. Use testing.allocator

test "dynamic allocation" {
    const allocator = testing.allocator;
    
    var list = std.ArrayList(i32).init(allocator);
    defer list.deinit();
    
    try list.append(42);
    try testing.expectEqual(@as(usize, 1), list.items.len);
}

3. Test Error Cases

fn divide(a: i32, b: i32) !i32 {
    if (b == 0) return error.DivisionByZero;
    return @divTrunc(a, b);
}

test "divide success" {
    try testing.expectEqual(5, try divide(10, 2));
}

test "divide by zero" {
    try testing.expectError(error.DivisionByZero, divide(10, 0));
}

4. Test Edge Cases

test "buffer edge cases" {
    // Empty
    try testing.expectEqual(@as(usize, 0), "".len);
    
    // Maximum size
    var large = try testing.allocator.alloc(u8, 1024 * 1024);
    defer testing.allocator.free(large);
    try testing.expectEqual(@as(usize, 1024 * 1024), large.len);
}
test "number parsing" {
    const cases = .{
        .{ .input = "42", .expected = 42 },
        .{ .input = "-10", .expected = -10 },
        .{ .input = "0", .expected = 0 },
    };
    
    inline for (cases) |case| {
        const result = try parseNumber(case.input);
        try testing.expectEqual(case.expected, result);
    }
}

Test Filtering

Run specific tests using filters:
# Run all tests
zig test myfile.zig

# Run tests matching a pattern
zig test myfile.zig --test-filter "math"

# Run a specific test
zig test myfile.zig --test-filter "math.add"

Random Seed for Tests

Tests have access to deterministic randomness:
lib/std/testing.zig:6-8
/// Provides deterministic randomness in unit tests.
/// Initialized on startup. Read-only after that.
pub var random_seed: u32 = 0;
const std = @import("std");
const testing = std.testing;

test "deterministic random" {
    var prng = std.Random.DefaultPrng.init(testing.random_seed);
    const random = prng.random();
    
    const value = random.int(u32);
    // Same seed produces same value across test runs
}

Testing I/O

The testing module provides I/O support:
lib/std/testing.zig:31-32
pub var io_instance: std.Io.Threaded = undefined;
pub const io = io_instance.io();

Backend Compatibility

lib/std/testing.zig:37-45
// Disable printing in tests for simple backends.
pub const backend_can_print = switch (builtin.zig_backend) {
    .stage2_aarch64,
    .stage2_powerpc,
    .stage2_riscv64,
    .stage2_spirv,
    => false,
    else => true,
};
Some backends have limited printing capabilities in tests.

Advanced Testing Patterns

Testing Generic Functions

fn max(comptime T: type, a: T, b: T) T {
    return if (a > b) a else b;
}

test "max with different types" {
    try testing.expectEqual(@as(i32, 10), max(i32, 5, 10));
    try testing.expectEqual(@as(f64, 3.14), max(f64, 2.71, 3.14));
    try testing.expectEqual(@as(u8, 255), max(u8, 100, 255));
}

Testing Async Code

test "async operations" {
    const result = comptime blk: {
        const x = async getValue();
        break :blk await x;
    };
    try testing.expectEqual(42, result);
}

Mocking and Test Doubles

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

const FileSystem = struct {
    readFn: *const fn ([]const u8) []const u8,
    
    pub fn read(self: FileSystem, path: []const u8) []const u8 {
        return self.readFn(path);
    }
};

fn mockRead(path: []const u8) []const u8 {
    _ = path;
    return "mock data";
}

test "with mock filesystem" {
    const fs = FileSystem{ .readFn = mockRead };
    const data = fs.read("/fake/path");
    try testing.expectEqualStrings("mock data", data);
}

Test Coverage

Generate code coverage reports:
zig test --test-coverage myfile.zig

Common Test Errors

Memory Leaks

test "will fail - memory leak" {
    const allocator = testing.allocator;
    _ = try allocator.alloc(u8, 100);
    // Missing free() - test will fail!
}

Incorrect Error Expected

test "will fail - wrong error" {
    // Function returns error.NotFound
    try testing.expectError(error.OutOfMemory, findItem());
    // Test fails: expected OutOfMemory, got NotFound
}

Summary

Zig’s testing framework provides:

Built-in Language Support

Tests are part of the language, not external tools

Memory Leak Detection

Automatic leak detection via testing.allocator

Rich Assertions

Comprehensive assertion functions with detailed output

Zero Overhead

Tests are completely omitted from production builds

Next Steps

Allocators

Deep dive into memory allocators

Key Modules

Explore essential standard library modules

Build docs developers (and LLMs) love