Testing
const std = @import("std");
test "expectEqual prints both values on failure" {
// expect(a == b) only tells you "false". expectEqual tells you
// expected 42, found 41. The expected value comes first.
try std.testing.expectEqual(42, 41 + 1);
try std.testing.expectEqual(@as(?u8, null), null);
}
test "slice and string comparisons show where they diverge" {
try std.testing.expectEqualSlices(u8, &.{ 1, 2, 3 }, &.{ 1, 2, 3 });
// On failure this prints both strings and the first differing index.
try std.testing.expectEqualStrings("hello", "hel" ++ "lo");
try std.testing.expectStringStartsWith("zig build verify", "zig build");
}
fn parseDigit(c: u8) !u4 {
if (c < '0' or c > '9') return error.NotADigit;
return @intCast(c - '0');
}
test "expectError asserts the failure path" {
// Untested error paths rot. This makes them first-class assertions.
try std.testing.expectError(error.NotADigit, parseDigit('x'));
try std.testing.expectEqual(7, try parseDigit('7'));
}
test "floats compare within a tolerance" {
const third: f64 = 1.0 / 3.0;
// Never == on computed floats; state how close is close enough.
try std.testing.expectApproxEqAbs(0.333, third, 0.001);
try std.testing.expectApproxEqRel(1.0, third * 3.0, std.math.floatEps(f64));
}
const Config = struct {
name: []const u8,
retries: u8,
};
test "expectEqualDeep follows pointers and slices" {
const a = Config{ .name = "prod", .retries = 3 };
const b = Config{ .name = "prod", .retries = 3 };
// expectEqual on these would compare the slice pointers.
// expectEqualDeep compares what they point at.
try std.testing.expectEqualDeep(a, b);
}
test "expectFmt checks formatted output" {
try std.testing.expectFmt("0x00ff", "0x{x:0>4}", .{255});
}
test "the test allocator reports leaks" {
// std.testing.allocator fails the test if anything is still
// allocated when the test returns. Forget this free and the test
// fails with a stack trace of the leaked allocation.
const gpa = std.testing.allocator;
const buf = try gpa.alloc(u8, 64);
defer gpa.free(buf);
// It also detects double-free and use-after-free in test builds.
try std.testing.expect(buf.len == 64);
}expect tells you nothing; the others tell you what
try std.testing.expect(a == b) fails with “expected true, found false” and
leaves you to add print statements. The typed assertions carry the values
into the failure message:
| Assertion | Reports on failure |
|---|---|
expectEqual(expected, actual) | both values |
expectEqualStrings | both strings and the first differing byte |
expectEqualSlices(T, ...) | both slices and the diverging index |
expectError(err, expr) | which error, or that none was returned |
The expected value comes first by convention. expectEqual infers its type
from that first argument, which is why @as(?u8, null) sometimes needs the
cast: it tells the comparison what type null is.
Assert the error paths too
Untested failure paths are where bugs hide. expectError makes “this input is
rejected” a first-class assertion, as checkable as the success case. A parser
test should pin both the digit it accepts and the character it refuses.
Floats need a tolerance
Never compare computed floats with ==. expectApproxEqAbs takes an absolute
tolerance for values near a known magnitude; expectApproxEqRel takes a
relative one, and std.math.floatEps(f64) is the natural bound for “as close
as the type allows.”
expectEqualDeep for structures
expectEqual on two structs holding slices compares the slice pointers, which
is almost never what you meant. expectEqualDeep follows pointers and slices
and compares the pointed-at content.
The allocator is a test
std.testing.allocator fails the test if anything it handed out is still live
when the test returns, so a missing free is a caught bug rather than a slow
leak in production. In test builds it also detects double-free and
use-after-free. Wire every allocation to a defer free and let the allocator
police it.