11 KiB
std.testing (Zig 0.16.0)
Unit testing utilities and assertions for Zig tests.
Primary Zig 0.16 release-note source: https://ziglang.org/download/0.16.0/release-notes.html
Use std.testing.io for tests that call file, process, networking, time, entropy, or I/O synchronization APIs:
const io = std.testing.io;
Fuzz tests now use *std.testing.Smith rather than raw []const u8 input.
Quick Reference
| Function | Purpose |
|---|---|
expect(bool) |
Assert condition is true |
expectEqual(expected, actual) |
Shallow equality (peer type resolution) |
expectEqualDeep(expected, actual) |
Deep equality (follows pointers, compares contents) |
expectEqualStrings(expected, actual) |
String equality with diff output |
expectEqualSlices(T, expected, actual) |
Slice equality with diff output |
expectError(error, result) |
Assert specific error returned |
expectApproxEqAbs/Rel(expected, actual, tolerance) |
Float comparison |
expectFmt(expected, template, args) |
Format string output |
expectStringStartsWith(actual, prefix) |
String prefix check |
expectStringEndsWith(actual, suffix) |
String suffix check |
Basic Assertions
const testing = std.testing;
// Boolean condition
try testing.expect(value > 0);
// Equality (uses peer type resolution)
try testing.expectEqual(expected, actual);
try testing.expectEqual(@as(u32, 42), some_u32);
// String equality (with visual diff on failure)
try testing.expectEqualStrings("hello", slice);
// String prefix/suffix
try testing.expectStringStartsWith(path, "/home/");
try testing.expectStringEndsWith(filename, ".zig");
// Slice equality (with visual diff, works with any element type)
try testing.expectEqualSlices(u8, expected_bytes, actual_bytes);
try testing.expectEqualSlices(u32, &[_]u32{1, 2, 3}, result_slice);
// Sentinel-terminated slice equality
try testing.expectEqualSentinel(u8, 0, expected_cstr, actual_cstr);
// Deep equality (recursively compares supported structs, arrays, and pointers)
try testing.expectEqualDeep(expected_struct, actual_struct);
// Float comparison (absolute tolerance)
try testing.expectApproxEqAbs(@as(f32, 1.0), result, 0.001);
// Float comparison (relative tolerance)
try testing.expectApproxEqRel(@as(f64, 100.0), result, 0.01);
expectEqual vs expectEqualDeep
const Point = struct { x: i32, y: i32 };
// expectEqual - recursively compares structs/arrays, but pointer and slice identity
const p1 = Point{ .x = 1, .y = 2 };
const p2 = Point{ .x = 1, .y = 2 };
try testing.expectEqual(p1, p2); // OK - structs compared field-by-field
// For slices, expectEqual compares ptr and len (identity)
const a = [_]u8{ 1, 2, 3 };
const b = [_]u8{ 1, 2, 3 };
// testing.expectEqual(&a, &b); // FAILS - different pointers
// expectEqualDeep - follows single-item pointers and compares supported contents
try testing.expectEqualDeep(&a, &b); // OK - compares contents
try testing.expectEqualDeep("abc", "abc"); // OK
expectEqualDeep is not cycle-aware: self-referential values can recurse indefinitely. C pointers, many-item pointers, function pointers, and opaque pointers are compared by identity rather than dereferenced.
Error Assertions
// Expect specific error
try testing.expectError(error.OutOfMemory, fallible_function());
// Unwrap or fail test (using try directly)
const value = try fallible_function(); // fails test on any error
Format Testing
// Test format string output
try testing.expectFmt("42", "{}", .{@as(u32, 42)});
try testing.expectFmt("hello world", "{s} {s}", .{"hello", "world"});
Testing Allocator
std.testing.allocator is a DebugAllocator (formerly GeneralPurposeAllocator) that detects memory leaks and use-after-free. Only available in test builds.
test "with allocator" {
// Detects leaks and use-after-free
var list: std.ArrayList(u32) = .empty;
defer list.deinit(testing.allocator);
try list.append(testing.allocator, 42);
try testing.expectEqual(@as(usize, 1), list.items.len);
}
// If defer is missing, test fails with leak report
Failing Allocator
std.testing.failing_allocator always returns error.OutOfMemory. Use for testing error paths:
test "handle allocation failure" {
try testing.expectError(
error.OutOfMemory,
testing.failing_allocator.alloc(u8, 100)
);
}
Configurable FailingAllocator
For controlled failure testing, use FailingAllocator to fail after N allocations:
test "fail on third allocation" {
var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{
.fail_index = 2, // First 2 allocations succeed, third fails
});
const allocator = failing.allocator();
const a = try allocator.create(i32); // succeeds (index 0)
defer allocator.destroy(a);
const b = try allocator.create(i32); // succeeds (index 1)
defer allocator.destroy(b);
try testing.expectError(error.OutOfMemory, allocator.create(i32)); // fails (index 2)
}
// Configuration options
var failing = std.testing.FailingAllocator.init(backing_allocator, .{
.fail_index = 5, // Fail on 6th allocation (default: never)
.resize_fail_index = 3, // Fail on 4th resize (default: never)
});
// Inspect state after use
std.debug.print("Allocated: {} bytes\n", .{failing.allocated_bytes});
std.debug.print("Freed: {} bytes\n", .{failing.freed_bytes});
std.debug.print("Allocations: {}\n", .{failing.allocations});
std.debug.print("Deallocations: {}\n", .{failing.deallocations});
Exhaustive Allocation Failure Testing
checkAllAllocationFailures tests that your code handles OutOfMemory at every allocation point without leaking. The tested function must take an allocator as its first argument and return !void; reset any shared state between runs:
fn myFunction(allocator: std.mem.Allocator, size: usize) !void {
var foo = try allocator.alloc(u8, size);
defer allocator.free(foo);
var bar = try allocator.alloc(u8, size);
defer allocator.free(bar);
// ... use foo and bar
}
test "no leaks on allocation failure" {
// Runs myFunction multiple times, failing each allocation in turn
try std.testing.checkAllAllocationFailures(
std.testing.allocator,
myFunction,
.{@as(usize, 10)}, // extra args tuple
);
}
How it works:
- Runs function once to count total allocations
- Runs N more times, failing allocation 0, then 1, then 2...
- Verifies
OutOfMemoryis returned and no memory leaked
Harness-specific errors include:
error.MemoryLeakDetected- allocation failed but memory wasn't freederror.SwallowedOutOfMemoryError-OutOfMemorywas caught but not propagatederror.NondeterministicMemoryUsage- allocation count varies between runs
Other errors returned by the tested function are propagated unchanged.
Temporary Directory
Create an isolated temp directory for file system tests:
test "file operations" {
var tmp = std.testing.tmpDir(.{}); // creates .zig-cache/tmp/<random>/
defer tmp.cleanup();
// Write and read files
const io = std.testing.io;
{
const file = try tmp.dir.createFile(io, "test.txt", .{});
defer file.close(io);
var write_buf: [256]u8 = undefined;
var file_writer = file.writer(io, &write_buf);
try file_writer.interface.writeAll("hello");
try file_writer.interface.flush();
}
// Use tmp.dir for all operations
const content = try tmp.dir.readFileAlloc(io, "test.txt", std.testing.allocator, .limited(1024));
defer std.testing.allocator.free(content);
try testing.expectEqualStrings("hello", content);
}
Test Organization
test "descriptive test name" {
// test body
}
test {
// Anonymous test, runs with others
}
// Reference other tests (pulls in tests from imported module)
test {
_ = @import("other_module.zig");
}
// Force semantic analysis of all declarations (catches unused code errors)
comptime {
std.testing.refAllDecls(@This());
}
// refAllDecls visits the immediate declarations of the supplied type.
// Zig 0.16 has no std.testing.refAllDeclsRecursive helper; recurse through
// selected nested types explicitly when that is part of the test's intent.
Skip Tests
test "skip this" {
return error.SkipZigTest;
}
test "conditional skip" {
if (builtin.os.tag == .windows) return error.SkipZigTest;
// ...
}
test "skip if feature unavailable" {
if (!@hasDecl(std.os, "linux")) return error.SkipZigTest;
// Linux-specific test...
}
Test Logging
test "with logging" {
// std.debug.print writes directly to stderr; it is not gated by log_level.
std.debug.print("Debug info: {}\n", .{value});
}
// Configurable log level for tests
// std.testing.log_level = .debug; // default is .warn
Deterministic Randomness
Tests have access to a deterministic random seed for reproducible "random" tests:
test "deterministic random" {
var prng = std.Random.DefaultPrng.init(std.testing.random_seed);
const random = prng.random();
const value = random.int(u32);
// Same seed = same value on every run
}
Fuzz Testing
test "fuzz parser" {
try std.testing.fuzz(
{}, // context (passed to test function)
struct {
fn testOne(_: void, smith: *std.testing.Smith) !void {
var storage: [4096]u8 = undefined;
const input = storage[0..smith.slice(&storage)];
_ = myParser.parse(input) catch |err| switch (err) {
error.InvalidInput => return, // expected
else => return err,
};
}
}.testOne,
.{
.corpus = &.{ // seed inputs
"valid input 1",
"valid input 2",
},
},
);
}
Common Patterns
Table-Driven Tests
test "parameterized" {
const cases = [_]struct { input: i32, expected: i32 }{
.{ .input = 0, .expected = 0 },
.{ .input = 1, .expected = 1 },
.{ .input = -1, .expected = 1 },
};
for (cases) |case| {
try testing.expectEqual(case.expected, abs(case.input));
}
}
Test Context/Fixture
const TestContext = struct {
allocator: std.mem.Allocator,
data: *Data,
fn init(ally: std.mem.Allocator) !TestContext {
const data = try ally.create(Data);
return .{ .allocator = ally, .data = data };
}
fn deinit(self: *TestContext) void {
self.allocator.destroy(self.data);
}
};
test "with context" {
var ctx = try TestContext.init(testing.allocator);
defer ctx.deinit();
// use ctx.data...
}
Testing with ArenaAllocator
test "arena for test allocations" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const ally = arena.allocator();
// No need for individual frees - arena handles cleanup
const a = try ally.alloc(u8, 100);
const b = try ally.alloc(u8, 200);
_ = a; _ = b;
// arena.deinit() frees everything
}
Running Tests
zig build test # Run all tests
zig test src/lib.zig # Test single file
zig test --test-filter "name" # Filter by name substring
zig test --help # List options supported by this Zig version