概述
好的测试简短、精确,并且言之有物。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
// 可以安全除法:使用 @divTrunc 进行截断整数除法
return @divTrunc(a, b);
}
test "boolean and equality expectations" {
// Test basic boolean expression using expect
// expect() returns an error if the condition is false
// 使用 expect 测试基本布尔表达式
// 如果条件为假,expect() 返回错误
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
// 使用 expectEqual 测试类型安全相等性
// 两个参数必须是相同类型;这里我们显式转换为 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 字节切片
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
// 使用 expectEqualStrings 进行切片比较
// 这比较切片的内容,而不仅仅是指针地址
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
// 测试 divExact 在除以零时返回期望的错误
// 如果函数返回指定的错误,expectError() 成功
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' 来解包成功值,然后使用 expectEqual 验证它
// 如果 divExact 在这里返回错误,测试将失败
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.
// 此测试故意泄漏以演示测试分配器的泄漏检测。
// 不要将此模式复制到真实代码中;有关修复方法,请参阅 leak_demo_fix.zig。
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" {
// Use the testing allocator, which tracks allocations and detects leaks
// 使用测试分配器,它跟踪分配并检测泄漏
const allocator = std.testing.allocator;
// Allocate a 64-byte buffer on the heap
// 在堆上分配 64 字节缓冲区
const buf = try allocator.alloc(u8, 64);
// Schedule deallocation to happen at scope exit (ensures cleanup)
// 计划在作用域退出时进行释放(确保清理)
defer allocator.free(buf);
// Fill the buffer with 0xAA pattern to demonstrate usage
// 用 0xAA 模式填充缓冲区以演示用法
for (buf) |*b| b.* = 0xAA;
// When the test exits, defer runs allocator.free(buf)
// The testing allocator verifies all allocations were freed
// 当测试退出时,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.
// 此处没有"坏"实现;有关专门的失败示例,请参阅 leak_demo_fail.zig。
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.
// 故意不包含:在 checkAllAllocationFailures 下的"坏"实现
// 将导致测试运行器因泄漏日志而失败,即使你期望错误。
// 有关专门的失败示例,请参阅 leak_demo_fail.zig。
$ zig test oom_injection.zigAll 1 tests passed.在checkAllAllocationFailures下一个故意“坏”的实现将导致测试运行器记录泄漏的分配并使整个运行失败,即使你expectError(error.MemoryLeakDetected, …)。在教学或调试时,请将失败的示例隔离开来;参见10。
注意与警告
练习
- 编写一个函数,从输入字节构建一个
std.ArrayList(u8),然后清除它。使用checkAllAllocationFailures来验证OOM安全性;参见11。 - 在第一次分配后引入一个故意的提前返回,并观察泄漏检测器捕捉到一个缺失的
free;然后用defer修复它。 - 为一个在无效输入上返回错误的函数添加
expectError测试;包括错误和成功路径。