Testing & Debugging

Testing is a language feature in Zig, not an add-on: 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 --check for 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.
Thinking in Zig: tests, safety checks, and leak detection form one coherent feedback loop: the compiler proves structure, the safe runtime proves behavior, and the testing allocator proves resource hygiene. Production code should carry the same rigor with ReleaseSafe — that is the Zig default for deliverables.

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.