Testing & Debugging
test blocks live inside your source files and run with zig test. The standard testing allocator turns memory leaks into test failures, and doctests let documentation stay correct forever.
Test Blocks
A test block is a named scope anywhere in a file; the compiler collects them all. They can be exported to consumers or kept private — the harness runs them wherever they are.
Declaring
const std = @import("std");
fn add(a: u32, b: u32) u32 {
return a + b;
}
test "add works" {
try std.testing.expectEqual(@as(u32, 5), add(2, 3));
}
zig test
zig test math.zig
# 1/1 math.test.add works... OK
# All 1 tests passed.
Assertions
The std.testing namespace provides typed checks. They report file, line, and actual versus expected values — far better than a bare panic.
expect and expectEqual
test "ordering checks" {
try std.testing.expect(1 < 2); // boolean condition
try std.testing.expectEqual(@as(u32, 3), 1 + 2); // equality, types shown
try std.testing.expectEqualStrings("ab", &[_]u8{ a, b });
}
Error-Passing Tests
Tests return !void, so they can try fallible code directly. An unexpected error marks the test failed with the error name — no setup ceremony needed.
test "file is readable" {
const data = try std.fs.cwd().readFileAlloc(std.testing.allocator, "notes.txt", 1000);
defer std.testing.allocator.free(data);
try std.testing.expect(data.len > 0);
}
Leak Detection
The best testing gift: std.testing.allocator verifies every allocation is freed. A test that leaks fails with the leak count and stack — the memory discipline of the memory lesson becomes a machine-checked rule.
std.testing.allocator
test "no leaks allowed" {
const buf = try std.testing.allocator.alloc(u8, 8);
// forget the free -> the test FAILS with a leak report
defer std.testing.allocator.free(buf);
}
GPA in Tests
For library code that receives an allocator, pass std.testing.allocator in every unit test. For binary code, create a GPA in the test and assert its deinit is leak-free.
Doctests
Documentation can rot. Zig doctests tie the two together: a test written in a doc comment runs in CI, so the example in the docs is proven every time the suite runs.
Doc Test Syntax
/// Multiply two integers.
///
/// `test "example" {
/// const std = @import("std");
/// try std.testing.expectEqual(@as(u32, 6), mul(2, 3));
/// }`
fn mul(a: u32, b: u32) u32 {
return a * b;
}
Debugging Tools
Beyond tests, four tools carry most debugging sessions.
fmt, Safety, Logs, and Traces
zig fmt— canonical formatting, with--checkfor CI.- Safety panics — overflow, bounds, and alignment failures print stack traces in Debug/ReleaseSafe.
std.log— leveled logging (std.log.info,.err) that routes to stderr.- Error return traces — the errors lesson showed how they name the failing call chain.
Common Pitfalls
Shared State Across Tests
Tests may run in any order. Reset globals at the start of every test block, or keep state inside the test — determinism matters more than speed.
Testing Only in Debug
Run the suite in ReleaseSafe at least once before shipping: safety checks behave identically, but the optimizer can expose bugs that Debug hides (uninitialized reads, for example).
Next, practice: the Lab Examples page collects runnable programs for every lesson in this track.