Zig has built-in support for tests.
A test is declared with the test keyword.
const std = @import("std");
test "addition" {
try std.testing.expect(1 + 1 == 2);
}Run the file with:
zig test main.zigIf the condition is true, the test passes.
If the condition is false, the test fails.
std.testing.expect checks a boolean expression.
try std.testing.expect(x == y);It returns an error when the expression is false, so it is usually called with try.
A test block can contain ordinary Zig code.
const std = @import("std");
fn add(a: i32, b: i32) i32 {
return a + b;
}
test "add returns the sum" {
try std.testing.expect(add(3, 4) == 7);
}Tests can be placed in the same file as the code they test.
This is common in Zig.
A more precise comparison uses expectEqual.
const std = @import("std");
test "equal integers" {
try std.testing.expectEqual(@as(u32, 42), @as(u32, 42));
}The first argument is the expected value.
The second argument is the actual value.
try std.testing.expectEqual(expected, actual);For slices, use expectEqualSlices.
const std = @import("std");
test "equal slices" {
const a = [_]u8{ 1, 2, 3 };
const b = [_]u8{ 1, 2, 3 };
try std.testing.expectEqualSlices(u8, a[0..], b[0..]);
}The first argument is the element type.
u8The next two arguments are the slices to compare.
For strings, use expectEqualStrings.
const std = @import("std");
test "equal strings" {
try std.testing.expectEqualStrings("zig", "zig");
}Tests can also check for errors.
const std = @import("std");
fn fail() !void {
return error.BadInput;
}
test "expected error" {
try std.testing.expectError(error.BadInput, fail());
}This test passes only if fail() returns error.BadInput.
The testing module also provides an allocator.
const std = @import("std");
test "array list" {
var list = std.ArrayList(u8).init(std.testing.allocator);
defer list.deinit();
try list.append(1);
try list.append(2);
try std.testing.expectEqualSlices(u8, &.{ 1, 2 }, list.items);
}std.testing.allocator is useful because the test runner can detect leaks.
If allocated memory is not freed, the test reports it.
This makes tests good places to check ownership.
A test can import other declarations from the same file.
const std = @import("std");
fn isEven(x: u32) bool {
return x % 2 == 0;
}
test "even numbers" {
try std.testing.expect(isEven(2));
try std.testing.expect(!isEven(3));
}Tests are declarations.
They can appear beside functions, structs, constants, and variables.
A file may contain many tests.
test "one" {
try std.testing.expect(true);
}
test "two" {
try std.testing.expect(10 > 3);
}All tests in the file are run by zig test.
For larger programs, tests can be run through the build system.
const std = @import("std");
pub fn build(b: *std.Build) void {
const tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = b.graph.host,
}),
});
const run_tests = b.addRunArtifact(tests);
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_tests.step);
}Then run:
zig build testThe test runner builds the test executable and runs it.
A useful pattern is to keep small tests close to the code.
fn clamp(x: i32, lo: i32, hi: i32) i32 {
if (x < lo) return lo;
if (x > hi) return hi;
return x;
}
test "clamp" {
try std.testing.expectEqual(@as(i32, 0), clamp(-5, 0, 10));
try std.testing.expectEqual(@as(i32, 5), clamp(5, 0, 10));
try std.testing.expectEqual(@as(i32, 10), clamp(15, 0, 10));
}The test describes the contract of the function.
It also provides a small example of use.
Tests should be simple.
They should build values, call functions, and check results.
The best tests make failure easy to locate.
Exercise 14-29. Write a test for a function that returns the larger of two integers.
Exercise 14-30. Write a test that compares two byte slices.
Exercise 14-31. Write a test that expects a specific error.
Exercise 14-32. Use std.testing.allocator in a test and free all allocated memory.