概述
好的测试简短、精确且言简意赅。Zig的std.testing通过小巧、可组合的断言(expect、expectEqual、expectError)和默认检测泄漏的内置测试分配器使这变得容易。结合分配失败注入,你可以执行否则难以触发的错误路径,确保你的代码正确且确定性地释放资源;参见10和testing.zig。
本章展示了如何编写表达性测试,如何解释测试运行器的泄漏诊断,以及如何使用std.testing.checkAllAllocationFailures来防弹代码对抗error.OutOfMemory,而无需编写数百个定制测试;参见11和heap.zig。
学习目标
使用std.testing进行测试基础
Zig的测试运行器在你传递给zig test的任何文件中发现test块。断言是返回错误的普通函数,因此它们自然地与try/catch组合。
std.testing 模块结构
在深入了解具体断言之前,了解 std.testing 中可用的完整工具包是很有帮助的。该模块提供三类功能:断言函数、测试分配器和实用工具。
本章重点介绍核心断言(expect、expectEqual、expectError)和用于泄漏检测的测试分配器。额外的断言函数如 expectEqualSlices 和 expectEqualStrings 提供专门的比较,而实用工具如 tmpDir() 帮助测试文件系统代码;参见 testing.zig。
期望:布尔值、相等性和错误
此示例涵盖布尔断言、值相等性、字符串相等性以及期望被测试函数返回错误。
const std = @import("std");
// / Performs exact integer division, returning an error if the divisor is zero.
// / This function demonstrates error handling in a testable way.
fn divExact(a: i32, b: i32) !i32 {
// Guard clause: check for division by zero before attempting division
if (b == 0) return error.DivideByZero;
// Safe to divide: use @divTrunc for truncating integer division
return @divTrunc(a, b);
}
test "boolean and equality expectations" {
// Test basic boolean expression using expect
// expect() returns an error if the condition is false
try std.testing.expect(2 + 2 == 4);
// Test type-safe equality with expectEqual
// Both arguments must be the same type; here we explicitly cast to u8
try std.testing.expectEqual(@as(u8, 42), @as(u8, 42));
}
test "string equality (bytes)" {
// Define expected string as a slice of const bytes
const expected: []const u8 = "hello";
// Create actual string via compile-time concatenation
// The ++ operator concatenates string literals at compile time
const actual: []const u8 = "he" ++ "llo";
// Use expectEqualStrings for slice comparison
// This compares the content of the slices, not just the pointer addresses
try std.testing.expectEqualStrings(expected, actual);
}
test "expecting an error" {
// Test that divExact returns the expected error when dividing by zero
// expectError() succeeds if the function returns the specified error
try std.testing.expectError(error.DivideByZero, divExact(1, 0));
// Test successful division path
// We use 'try' to unwrap the success value, then expectEqual to verify it
// If divExact returns an error here, the test will fail
try std.testing.expectEqual(@as(i32, 3), try divExact(9, 3));
}
$ zig test basic_tests.zigAll 3 tests passed.通过构造进行泄漏检测
测试分配器(std.testing.allocator)是一个配置为跟踪分配并在测试完成时报告泄漏的 GeneralPurposeAllocator。这意味着如果你的测试忘记释放内存,它们就会失败;参见 10。
测试分配器如何工作
测试模块提供两个分配器:用于一般测试并带有泄漏检测的 allocator,以及用于模拟分配失败的 failing_allocator。了解它们的架构有助于解释它们的不同行为。
testing.allocator 包装了一个配置有堆栈跟踪和泄漏检测的 GeneralPurposeAllocator。failing_allocator 使用 FixedBufferAllocator 作为其基础,然后用失败注入逻辑包装它。两者都暴露标准的 Allocator 接口,使它们成为测试中生产分配器的即插即用替代品;参见 testing.zig。
泄漏的样子
下面的测试故意忘记 free。运行器报告泄漏的地址、分配调用点的堆栈跟踪,并以非零状态退出。
const std = @import("std");
// This test intentionally leaks to demonstrate the testing allocator's leak detection.
// Do NOT copy this pattern into real code; see leak_demo_fix.zig for the fix.
test "leak detection catches a missing free" {
const allocator = std.testing.allocator;
// Intentionally leak this allocation by not freeing it.
const buf = try allocator.alloc(u8, 64);
// Touch the memory so optimizers can't elide the allocation.
for (buf) |*b| b.* = 0xAA;
// No free on purpose:
// allocator.free(buf);
}
$ zig test leak_demo_fail.zig[gpa] (err): memory address 0x… leaked:
… leak_demo_fail.zig:1:36: … in test.leak detection catches a missing free (leak_demo_fail.zig)
All 1 tests passed.
1 errors were logged.
1 tests leaked memory.
error: the following test command failed with exit code 1:
…/test --seed=0x…"All N tests passed." 行仅断言测试逻辑;泄漏报告仍然导致整体运行失败。修复泄漏以使测试套件变为绿色。04
使用 defer 修复泄漏
在成功分配后立即使用 defer allocator.free(buf) 来保证在所有路径上释放。
const std = @import("std");
test "no leak when freeing properly" {
// 使用测试分配器,它跟踪分配并检测泄漏
const allocator = std.testing.allocator;
// 在堆上分配64字节缓冲区
const buf = try allocator.alloc(u8, 64);
// 安排在作用域退出时释放(确保清理)
defer allocator.free(buf);
// 用0xAA模式填充缓冲区以演示用法
for (buf) |*b| b.* = 0xAA;
// 当测试退出时,defer运行allocator.free(buf)
// 测试分配器验证所有分配都被释放
}
$ zig test leak_demo_fix.zigAll 1 tests passed.泄漏检测生命周期
泄漏检测在每个测试结束时自动发生。理解这个时间线有助于解释为什么 defer 必须在测试完成前执行,以及为什么即使测试断言通过也会出现泄漏报告。
当测试结束时,GeneralPurposeAllocator 验证所有分配的内存是否已被释放。如果任何分配仍然存在,它会打印堆栈跟踪显示泄漏内存被分配的位置(而不是应该被释放的位置)。这种自动检查消除了整个类别的错误,无需手动跟踪。关键是在成功分配后立即放置 defer allocator.free(…),以便它在所有代码路径上执行,包括提前返回和错误传播;参见 heap.zig。
分配失败注入
分配内存的代码即使在分配失败时也必须是正确的。std.testing.checkAllAllocationFailures 在每个分配点使用失败分配器重新运行你的函数,验证你清理了部分初始化的状态并正确传播 error.OutOfMemory;参见 10。
系统性地测试OOM安全性
此示例使用 checkAllAllocationFailures 和一个执行两次分配并使用 defer 释放两者的小函数。该助手在每个分配点模拟失败;只有在没有泄漏发生且 error.OutOfMemory 被正确转发时,测试才会通过。
const std = @import("std");
fn testImplGood(allocator: std.mem.Allocator, length: usize) !void {
const a = try allocator.alloc(u8, length);
defer allocator.free(a);
const b = try allocator.alloc(u8, length);
defer allocator.free(b);
}
// No "bad" implementation here; see leak_demo_fail.zig for a dedicated failing example.
test "OOM injection: good implementation is leak-free" {
const allocator = std.testing.allocator;
try std.testing.checkAllAllocationFailures(allocator, testImplGood, .{32});
}
// Intentionally not included: a "bad" implementation under checkAllAllocationFailures
// will cause the test runner to fail due to leak logging, even if you expect the error.
// See leak_demo_fail.zig for a dedicated failing example.
$ zig test oom_injection.zigAll 1 tests passed.在 checkAllAllocationFailures 下故意实现的"坏"实现将导致测试运行器记录泄漏的分配并使整体运行失败,即使你 expectError(error.MemoryLeakDetected, …)。在教学或调试时,将失败的示例隔离;参见 10。
注意事项
练习
- 编写一个函数,从输入字节构建
std.ArrayList(u8),然后清除它。使用checkAllAllocationFailures验证OOM安全性;参见 11。 - 在第一次分配后故意引入提前返回,观察泄漏检测器捕获缺失的
free;然后使用defer修复它。 - 为在无效输入时返回错误的函数添加
expectError测试;包括错误路径和成功路径。