第8章 单元测试 (Unit Tests)
第8章 单元测试 (Unit Tests)
编写高质量的代码离不开测试。Zig 从设计之初就将单元测试作为一等公民,内置了一个强大且灵活的测试框架。本章将全面讲解如何在 Zig 中编写、组织和运行单元测试。
1. test 块:你的测试入口
在 Zig 中,所有的单元测试都写在 test 块中。一个 test 块就是一个独立的测试用例。
const std = @import("std"); // 导入标准库
const expect = std.testing.expect; // 导入常用的 expect 函数
test "简单的加法测试" { // test 关键字后面可以跟一个描述性的字符串
const a: u8 = 2;
const b: u8 = 2;
try expect((a + b) == 4); // 使用 expect 进行断言
}
test关键字:声明一个测试块。后面可以跟一个字符串字面量作为测试名称。try expect(...):这是最常用的断言函数。如果括号内的表达式结果为true,则测试通过;如果为false,则测试失败。由于expect可能会返回错误,所以需要用try。- 代码混排:你可以将
test块直接写在你的源代码文件中。当使用zig build、zig build-exe等命令编译项目时,编译器会自动忽略test块中的代码,只进行语法检查。只有运行zig test命令时,这些测试才会被编译和执行。
最佳实践:将单元测试与它所测试的函数或类型放在同一个文件中,这能让测试更贴近其所验证的代码逻辑。
2. 运行测试
使用 zig test 命令编译并运行你的测试:
zig test your_module.zig
如果你在一个 zig build 的项目中,也可以直接运行:
zig build test
编译器会查找所有 .zig 文件中的 test 块,编译它们,然后顺序执行。
3. 测试内存分配与泄漏
Zig 强大的地方在于它能帮助我们检测内存问题。在测试堆内存分配时,你可以使用特殊的 测试分配器 (std.testing.allocator) 来自动检测内存泄漏。
这个分配器会跟踪所有由它分配的内存,并在其生命周期结束时检查是否有未释放的内存。
const std = @import("std");
const Allocator = std.mem.Allocator;
fn may_leak(allocator: Allocator) !void {
// 假设这个函数会在堆上分配内存但忘记释放
const buffer = try allocator.alloc(u32, 10);
_ = buffer; // 分配了但没有 free
// ... 函数返回
}
test "检测内存泄漏" {
// 使用 std.testing.allocator
const test_allocator = std.testing.allocator;
// 如果 may_leak 真的泄漏了,test_allocator 会在测试结束时报告错误
try may_leak(test_allocator);
// 如果没有泄漏,gpa.deinit() 会检查并清理
// (std.testing.allocator 在内部会调用 GeneralPurposeAllocator 并进行 deinit)
}
运行 zig test,如果 may_leak 确实有内存泄漏,你会看到类似以下的错误报告:
[gpa] (err): memory address 0x... leaked:
your_module.zig:X:Y: ...
4. 测试错误 (Error Testing)
在 Zig 中,函数经常会返回 !T (错误联合类型)。测试这些错误返回也是非常重要的。
std.testing.expectError() 函数可以帮助你精确地测试一个函数是否返回了特定类型的错误。
const std = @import("std");
const expectError = std.testing.expectError;
// 一个可能会返回 OutOfMemory 错误的函数
fn try_alloc_large(allocator: Allocator) !void {
_ = try allocator.alloc(u8, 1024 * 1024); // 尝试分配 1MB
}
test "测试 OutOfMemory 错误" {
// 使用一个非常小的固定缓冲区分配器
var tiny_buffer: [10]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&tiny_buffer);
const allocator = fba.allocator();
// 预期 try_alloc_large 会返回 OutOfMemory 错误
try expectError(error.OutOfMemory, try_alloc_large(allocator));
}
5. 常用断言函数
除了 expect() 之外,std.testing 模块还提供了许多方便的断言函数:
std.testing.expectEqual(expected, actual):- 测试两个值是否相等。
- 不支持数组或切片。
test "expectEqual 示例" {
try std.testing.expectEqual(10, 5 + 5);
}
std.testing.expectEqualSlices(T, expected_slice, actual_slice):- 测试两个切片(或数组)的内容是否完全相同。
- 需要传入切片的元素类型
T。
test "expectEqualSlices 示例" {
const arr1 = [_]u8{1, 2, 3};
const arr2 = [_]u8{1, 2, 3};
try std.testing.expectEqualSlices(u8, &arr1, &arr2);
}
std.testing.expectEqualStrings(expected_string, actual_string):- 专门用于比较两个字符串(
[]const u8)是否相等。 - 失败时会输出详细的差异信息。
- 专门用于比较两个字符串(
test "expectEqualStrings 示例" {
const s1 = "Hello, Zig!";
const s2 = "Hello, Zig!";
try std.testing.expectEqualStrings(s1, s2);
}
总结
Zig 的内置测试框架简洁而强大:
- 方便集成:
test块直接与源代码混排。 - 默认安全:
std.testing.allocator自动检测内存泄漏。 - 精确断言:提供
expect、expectError、expectEqual等多种断言。 - 易于运行:
zig test命令一键执行所有测试。
熟练掌握单元测试,是确保你的 Zig 代码质量和可靠性的基石。