Zig 测试框架完全教程
1. 测试基础
Section titled “1. 测试基础”1.1 基本测试声明
Section titled “1.1 基本测试声明”const std = @import("std");
// 命名测试(字符串字面量)test "加法测试" { try std.testing.expect(add(2, 3) == 5);}
// 文档测试(标识符)test add { // 这个测试同时作为 add 函数的文档示例 try std.testing.expect(add(41, 1) == 42);}
/// 两个数相加fn add(a: i32, b: i32) i32 { return a + b;}1.2 运行测试
Section titled “1.2 运行测试”# 运行所有测试zig test math.zig
# 输出示例:# 1/2 测试.加法测试...OK# 2/2 测试.decltest.add...OK# 所有 2 个测试通过2. 测试声明详解
Section titled “2. 测试声明详解”2.1 测试声明特性
Section titled “2.1 测试声明特性”- 隐式返回类型:
anyerror!void - 顺序无关: 可以放在任何位置
- 仅测试构建: 只有
zig test时才包含 - 可放在单独文件: 不需要和被测代码放一起
2.2 文档测试
Section titled “2.2 文档测试”/// 计算斐波那契数/// 示例:/// ```zig/// const fib = fibonacci(5);/// try expect(fib == 5);/// ```pub fn fibonacci(n: u32) u32 { if (n < 2) return n; return fibonacci(n - 1) + fibonacci(n - 2);}
// 文档测试(自动关联到 fibonacci 函数)test fibonacci { try std.testing.expect(fibonacci(0) == 0); try std.testing.expect(fibonacci(1) == 1); try std.testing.expect(fibonacci(5) == 5); try std.testing.expect(fibonacci(10) == 55);}3. 测试失败处理
Section titled “3. 测试失败处理”3.1 测试失败示例
Section titled “3.1 测试失败示例”test "失败的测试" { // 测试失败会显示错误堆栈跟踪 try std.testing.expect(1 + 1 == 3);}
test "成功的测试" { try std.testing.expect(1 + 1 == 2);}输出:
1/2 测试.失败的测试...FAIL (TestUnexpectedResult)测试文件.zig:3:5: 0x102f078 in 测试.失败的测试 try std.testing.expect(1 + 1 == 3); ^2/2 测试.成功的测试...OK1 个通过; 0 个跳过; 1 个失败.3.2 测试错误处理
Section titled “3.2 测试错误处理”test "可能失败的测试" { const result = mightFail(); if (result) |value| { try std.testing.expect(value == 42); } else |err| { // 测试中处理错误 std.debug.print("测试失败: {}\n", .{err}); return err; }}
fn mightFail() !i32 { return error.NotImplemented;}4. 跳过测试
Section titled “4. 跳过测试”4.1 命令行过滤
Section titled “4.1 命令行过滤”# 只运行包含"math"的测试zig test my_tests.zig --test-filter math
# 只运行特定测试zig test my_tests.zig --test-filter "测试加法"4.2 编程式跳过
Section titled “4.2 编程式跳过”test "需要特定环境的测试" { // 检查运行环境 if (!hasRequiredFeature()) { std.debug.print("跳过: 需要特定功能\n", .{}); return error.SkipZigTest; }
try std.testing.expect(doFeatureTest() == true);}
test "总是跳过" { // 明确跳过 return error.SkipZigTest;}5. 内存泄漏检测
Section titled “5. 内存泄漏检测”5.1 使用测试分配器
Section titled “5.1 使用测试分配器”const std = @import("std");
test "检测内存泄漏" { const allocator = std.testing.allocator;
// 正确: 使用 defer 确保释放 var list1 = std.ArrayList(u8).init(allocator); defer list1.deinit(); // ✅ 确保释放 try list1.appendSlice("hello");
// 错误: 忘记 deinit (会被检测到) var list2 = std.ArrayList(u8).init(allocator); // 忘记: defer list2.deinit(); try list2.appendSlice("world");
try std.testing.expect(list1.items.len == 5);}5.2 泄漏检测输出
Section titled “5.2 泄漏检测输出”1/1 测试.检测内存泄漏...OK[gpa] (err): 内存地址 0x7f74a8aa0000 泄漏:文件路径:行号:列号: 0x10aa8fe in 函数名 const new_memory = try self.allocator.alignedAlloc(T, alignment, new_capacity); ^...所有 1 个测试通过.1 个错误被记录.1 个测试泄漏内存.6. 检测测试构建
Section titled “6. 检测测试构建”const std = @import("std");const builtin = @import("builtin");
test "只在测试中运行" { // 使用 @import("builtin").is_test 检测 if (builtin.is_test) { // 测试专用代码 try testSpecificLogic(); }}
// 条件编译测试代码fn debugHelper() void { // 只在测试构建中编译 comptime { if (!builtin.is_test) { @compileError("此函数只能在测试中使用"); } }
// 调试代码...}7. 测试辅助函数
Section titled “7. 测试辅助函数”7.1 常用断言函数
Section titled “7.1 常用断言函数”const std = @import("std");const expect = std.testing.expect;const expectEqual = std.testing.expectEqual;const expectError = std.testing.expectError;const expectEqualStrings = std.testing.expectEqualStrings;
test "各种断言" { // 基本断言 try expect(true); try expect(1 + 1 == 2);
// 相等断言(自动类型转换) const expected: i32 = 42; const actual: i64 = 42; try expectEqual(expected, actual); // i64 转换为 i32
// 字符串相等 try expectEqualStrings("hello", "hello");
// 错误断言 const result: anyerror!void = error.SomeError; try expectError(error.SomeError, result);
// 可选类型断言 const maybe_value: ?i32 = 42; try expect(maybe_value != null); try expectEqual(42, maybe_value.?);
// 浮点数比较(带误差) const a: f64 = 0.1 + 0.2; const b: f64 = 0.3; try expect(std.math.approxEqAbs(f64, a, b, 0.0000001));}7.2 切片和数组比较
Section titled “7.2 切片和数组比较”test "切片比较" { const expected = [_]i32{1, 2, 3, 4, 5}; const actual = [_]i32{1, 2, 3, 4, 5};
try std.testing.expectEqualSlices(i32, &expected, &actual);
// 也可以直接比较 try expect(std.mem.eql(i32, &expected, &actual));}
test "字符串比较" { const str1 = "hello"; const str2 = "hello";
try std.testing.expectEqualStrings(str1, str2); // 或 try expect(std.mem.eql(u8, str1, str2));}8. 高级测试模式
Section titled “8. 高级测试模式”8.1 测试夹具模式
Section titled “8.1 测试夹具模式”const TestContext = struct { allocator: std.mem.Allocator, temp_buffer: [1024]u8,
pub fn init() TestContext { return .{ .allocator = std.testing.allocator, .temp_buffer = undefined, }; }
pub fn createString(self: *TestContext, text: []const u8) ![]u8 { return try self.allocator.dupe(u8, text); }};
test "使用测试夹具" { var ctx = TestContext.init(); defer { // 清理资源... }
const str = try ctx.createString("test"); defer ctx.allocator.free(str);
try expectEqualStrings("test", str);}8.2 参数化测试
Section titled “8.2 参数化测试”const TestCase = struct { input: i32, expected: i32,};
test "参数化测试示例" { const cases = [_]TestCase{ .{ .input = 0, .expected = 0 }, .{ .input = 1, .expected = 1 }, .{ .input = 5, .expected = 5 }, .{ .input = 10, .expected = 55 }, };
for (cases) |case| { const actual = fibonacci(case.input); try expectEqual(case.expected, actual); }}8.3 模拟和存根
Section titled “8.3 模拟和存根”// 定义接口const Database = struct { const Self = @This();
query: *const fn (self: *Self, sql: []const u8) ![]const u8,
pub fn init() Self { return .{ .query = defaultQuery, }; }
fn defaultQuery(self: *Self, sql: []const u8) ![]const u8 { _ = self; _ = sql; return error.NotImplemented; }};
test "使用模拟数据库" { var mock_db = Database.init();
// 替换为模拟实现 mock_db.query = struct { fn mockQuery(_: *Database, sql: []const u8) ![]const u8 { if (std.mem.eql(u8, sql, "SELECT * FROM users")) { return "模拟数据"; } return error.QueryFailed; } }.mockQuery;
const result = try mock_db.query("SELECT * FROM users"); try expectEqualStrings("模拟数据", result);}9. 测试组织
Section titled “9. 测试组织”9.1 测试套件
Section titled “9.1 测试套件”// math.zig - 被测代码pub fn add(a: i32, b: i32) i32 { return a + b;}
pub fn multiply(a: i32, b: i32) i32 { return a * b;}
// tests/math_test.zig - 测试文件const std = @import("std");const math = @import("../math.zig");
test "加法测试套件" { // 分组相关测试 try std.testing.expect(math.add(0, 0) == 0); try std.testing.expect(math.add(1, 2) == 3); try std.testing.expect(math.add(-1, 1) == 0);}
test "乘法测试套件" { try std.testing.expect(math.multiply(0, 5) == 0); try std.testing.expect(math.multiply(3, 4) == 12); try std.testing.expect(math.multiply(-2, 3) == -6);}9.2 使用构建系统运行测试
Section titled “9.2 使用构建系统运行测试”const std = @import("std");
pub fn build(b: *std.Build) void { const test_step = b.step("test", "运行所有测试");
// 单元测试 const unit_tests = b.addTest(.{ .root_source_file = b.path("src/main.zig"), .target = b.graph.host, });
const run_unit_tests = b.addRunArtifact(unit_tests); test_step.dependOn(&run_unit_tests.step);
// 集成测试 const integration_tests = b.addTest(.{ .root_source_file = b.path("tests/integration.zig"), .target = b.graph.host, });
const run_integration_tests = b.addRunArtifact(integration_tests); test_step.dependOn(&run_integration_tests.step);
// 跨平台测试 const cross_targets = [_]std.Target.Query{ .{ .cpu_arch = .x86_64, .os_tag = .linux }, .{ .cpu_arch = .x86_64, .os_tag = .windows }, .{ .cpu_arch = .aarch64, .os_tag = .macos }, };
for (cross_targets) |target| { const cross_test = b.addTest(.{ .root_source_file = b.path("src/main.zig"), .target = b.resolveTargetQuery(target), });
const run_cross_test = b.addRunArtifact(cross_test); run_cross_test.skip_foreign_checks = true; test_step.dependOn(&run_cross_test.step); }}10. 测试最佳实践
Section titled “10. 测试最佳实践”10.1 测试命名规范
Section titled “10.1 测试命名规范”// 好的测试命名test "user_creation_succeeds_with_valid_data" {...}test "database_connection_fails_when_offline" {...}test "api_returns_404_for_nonexistent_resource" {...}
// 使用 Given-When-Then 模式test "given_valid_credentials_when_login_then_returns_token" { // Given: 准备测试数据 const credentials = Credentials{ .username = "alice", .password = "secret" };
// When: 执行操作 const result = try login(credentials);
// Then: 验证结果 try expect(result.token.len > 0); try expect(result.expires_at > std.time.timestamp());}10.2 测试隔离
Section titled “10.2 测试隔离”// 每个测试独立运行,不共享状态test "独立测试 1" { var state = State.init(std.testing.allocator); defer state.deinit();
// 修改状态 try state.addItem("test1"); try expect(state.count() == 1);}
test "独立测试 2" { // 重新创建状态,不受前一个测试影响 var state = State.init(std.testing.allocator); defer state.deinit();
try expect(state.count() == 0); // 总是从干净状态开始}10.3 性能测试
Section titled “10.3 性能测试”test "性能基准" { const iterations = 1000000;
const start = std.time.nanoTimestamp();
var sum: i64 = 0; for (0..iterations) |i| { sum += @as(i64, i); }
const end = std.time.nanoTimestamp(); const elapsed_ns = end - start; const elapsed_ms = @as(f64, @floatFromInt(elapsed_ns)) / 1_000_000.0;
std.debug.print("执行 {d} 次循环耗时: {d:.2} ms\n", .{ iterations, elapsed_ms }); try expect(sum > 0); // 确保循环实际执行}11. 测试命令行选项
Section titled “11. 测试命令行选项”# 查看所有测试选项zig test --help
# 常用选项:
# 过滤测试zig test --test-filter "math"
# 设置随机种子(用于重现随机失败)zig test --seed 0x12345678
# 详细输出zig test --verbose
# 并行运行测试(默认)zig test -j 4
# 不并行运行zig test --single-threaded
# 内存限制zig test --maxrss 1024M
# 跳过内存不足的测试zig test --skip-oom-steps
# 监视模式(文件变化时重新运行)zig test --watch
# 生成覆盖率报告(需要编译标志)zig test -fcoverage
# 测试报告格式zig test --summary all # 完整报告zig test --summary failures # 只显示失败(默认)zig test --summary none # 不显示报告12. 完整示例
Section titled “12. 完整示例”12.1 完整的数学库测试
Section titled “12.1 完整的数学库测试”const std = @import("std");
pub fn add(a: i32, b: i32) i32 { return a + b;}
pub fn subtract(a: i32, b: i32) i32 { return a - b;}
pub fn multiply(a: i32, b: i32) i32 { return a * b;}
pub fn divide(a: i32, b: i32) !i32 { if (b == 0) return error.DivisionByZero; return @divTrunc(a, b);}
// 文档测试test add { try std.testing.expect(add(2, 3) == 5); try std.testing.expect(add(-1, 1) == 0); try std.testing.expect(add(0, 0) == 0);}
test subtract { try std.testing.expect(subtract(5, 3) == 2); try std.testing.expect(subtract(0, 5) == -5);}
test multiply { try std.testing.expect(multiply(3, 4) == 12); try std.testing.expect(multiply(0, 100) == 0); try std.testing.expect(multiply(-2, 3) == -6);}
test "除法测试" { // 正常除法 try std.testing.expectEqual(5, try divide(10, 2)); try std.testing.expectEqual(-3, try divide(9, -3));
// 除零错误 try std.testing.expectError(error.DivisionByZero, divide(10, 0));}
test "边界情况" { const max = std.math.maxInt(i32); const min = std.math.minInt(i32);
// 溢出检测(如果 add 会溢出,需要错误处理) // try std.testing.expectError(error.Overflow, safeAdd(max, 1));
// 边界值测试 try std.testing.expect(subtract(max, max) == 0); try std.testing.expect(multiply(1, min) == min);}
test "内存安全" { const allocator = std.testing.allocator;
// 确保没有内存泄漏 var numbers = std.ArrayList(i32).init(allocator); defer numbers.deinit();
for (0..100) |i| { try numbers.append(@intCast(i)); }
try std.testing.expect(numbers.items.len == 100);}Zig 测试框架的关键特性:
- 简单直观: 只需
test关键字和断言 - 文档集成: 文档测试自动关联和验证
- 内存安全: 自动检测内存泄漏
- 灵活控制: 可跳过、过滤、参数化测试
- 丰富断言: 多种专用断言函数
- 构建集成: 可与构建系统深度集成
通过遵循最佳实践,你可以创建可靠、可维护的测试套件,确保代码质量并防止回归错误。