第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 buildzig 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 的内置测试框架简洁而强大:

  1. 方便集成test 块直接与源代码混排。
  2. 默认安全std.testing.allocator 自动检测内存泄漏。
  3. 精确断言:提供 expectexpectErrorexpectEqual 等多种断言。
  4. 易于运行zig test 命令一键执行所有测试。

熟练掌握单元测试,是确保你的 Zig 代码质量和可靠性的基石。


原文出处: https://pedropark99.github.io/zig-book/