Skip to content

Day 30: std.testing模块:测试框架

欢迎来到第三十天!我们已经完成了第一个月的学习。今天,我们将学习一个对于保证代码质量至关重要的主题:测试。Zig语言内置了一个简单而强大的测试框架,它与语言本身无缝集成。你不需要安装任何第三方的测试运行器或断言库。测试代码可以和你的应用代码放在同一个文件中,这使得编写和维护测试变得极其方便。今天,我们将学习如何定义测试用例、如何运行它们,以及如何使用 std.testing 模块提供的工具来验证代码的正确性。

在Zig中,一个测试用例是通过 test 关键字来定义的。它后面跟着一个描述测试目的的字符串字面量,以及一个代码块。

const std = @import("std");
fn add(a: i32, b: i32) i32 {
return a + b;
}
// 这是一个测试块
test "simple addition" {
const expected: i32 = 4;
const actual = add(2, 2);
try std.testing.expectEqual(expected, actual);
}
  • test "..." { ... } 就是一个测试用例。
  • 测试代码块的返回类型必须是 !void,这意味着测试可能会失败(返回一个错误)。
  • std.testing 模块提供了一系列 expect 函数(断言)来验证结果。
  • try std.testing.expectEqual(a, b): 如果 a 和 b 不相等,这个函数会返回一个错误,导致测试失败。try 会将这个错误传播出去。

要运行一个文件或整个项目中的所有测试,你可以使用 zig test 命令。

Terminal window
# 运行 my_file.zig 中的所有测试
zig test src/my_file.zig
# 使用构建系统运行整个项目的所有测试
zig build test

当 zig test 被调用时,编译器会:

  1. 找到所有的 test "..." {} 块。
  2. 将它们编译成一个特殊的可执行文件。
  3. 运行这个可执行文件,并报告每个测试的成功或失败。

如果所有 expect 都通过,测试就通过。如果任何一个 expect 函数返回了错误,测试就失败,并且你会看到详细的失败信息,包括文件名、行号和期望值与实际值。

std.testing 提供了一系列有用的断言函数:

  • expectEqual(expected, actual): 验证两个值是否相等。
  • expect(condition: bool): 验证一个布尔条件是否为 true。
  • expectError(expected_error, expression): 验证一个表达式是否返回了指定的错误。
  • expectEqualSlices(T: type, expected: []const T, actual: []const T): 专门用于比较两个切片的内容是否相等。
  • allocator: 一个特殊的分配器,用于在测试中自动检测内存泄漏(我们在Day 18见过它)。

通常,测试代码会和它所测试的代码放在同一个文件中。这使得在修改代码时很容易同时更新测试。

src/math.zig
const std = @import("std");
pub fn gcd(a: u32, b: u32) u32 {
var x = a;
var y = b;
while (y != 0) {
const temp = y;
y = x % y;
x = temp;
}
return x;
}
// math.zig 文件内的测试
test "gcd function" {
try std.testing.expectEqual(@as(u32, 5), gcd(10, 15));
try std.testing.expectEqual(@as(u32, 7), gcd(21, 49));
try std.testing.expectEqual(@as(u32, 1), gcd(17, 19));
}
test "gcd with zero" {
try std.testing.expectEqual(@as(u32, 10), gcd(10, 0));
try std.testing.expectEqual(@as(u32, 10), gcd(0, 10));
}

6. 实践练习:为Day 22的泛型栈编写测试

Section titled “6. 实践练习:为Day 22的泛型栈编写测试”

回到第22天实现的泛型 Stack。

  1. 在同一个文件中,添加一个 test "generic stack" 块。
  2. 在测试中,实例化一个 Stack(i32)。
  3. 使用 std.testing.allocator 来初始化栈,以自动检测内存泄漏。
  4. 测试 push 和 pop 操作的正确性。例如,push 一个值,然后 pop 出来,验证值是否与你 push 的相同。
  5. 测试 pop 一个空栈时,返回的是 null。

问:如何模拟(Mock)依赖?

答:Zig的测试框架没有内置的模拟或桩(stub)功能。但是,Zig的接口(VTable)和依赖注入模式使得模拟变得相对容易。如果你的函数接受一个接口(比如 Reader 或 Allocator),在测试中,你可以传入一个你手动实现的、符合该接口的“假”对象,这个假对象可以返回预设的数据或记录函数的调用情况。

问:测试代码会包含在最终的发布版本中吗?

答:不会。当你进行正常的构建(例如 zig build 或 zig build -Doptimize=ReleaseSafe)时,编译器会自动忽略所有的 test "..." {} 块。它们只在 zig test 或 zig build test 命令下才会被编译。

今天,我们学习了Zig简洁而强大的内置测试框架。我们掌握了如何使用 test 关键字定义测试用例,如何使用 std.testing 中的 expect 函数进行断言,以及如何通过 zig test 命令来运行测试。将测试作为语言的一等公民,极大地鼓励了开发者编写测试,从而提高了代码的质量和可维护性。测试驱动开发(TDD)在Zig中是一个非常自然和高效的流程。

祝贺你完成了90天计划的第一个月!你已经掌握了Zig语言的核心语法、内存模型和关键工具。从明天开始,我们将进入第二阶段,深入探索标准库中更多强大的模块,从**std.crypto:加密基础**开始。