Last time I was coding in Typescript, I get used to writing my own testing framework. In Zig, however, the test runner is already provided through the use of test {}
declaration.
Below I wrote an example where I struggle to decide (futurewise) whether I should use what is given, ie. test "caseName" {...}
, or I should create a dumb main()
, put inside the things I want to test, and control everything myself.
Zig-way:
test "EmptyString" {
const actual = try utils.String.lineBefore(0, "", std.testing.allocator);
defer actual.deinit();
try std.testing.expectEqualSlices(u8, "", actual.items);
}
test "FirstChar" {
const actual = try utils.String.lineBefore(0, "bar", std.testing.allocator);
defer actual.deinit();
try std.testing.expectEqualSlices(u8, "", actual.items);
}
test "OutOfBound" {
const actual = try utils.String.lineBefore(1000, "bar", std.testing.allocator);
defer actual.deinit();
try std.testing.expectEqualSlices(u8, "ba", actual.items);
}
Custom-way (let’s say, this is oversimplified version, without stats, ability to skip or focus on particular test cases, show/suppress succeeded cases, etc):
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const alloc = gpa.allocator();
const cases = [_]struct { name: []const u8, at: u8, input: []const u8, expected: []const u8 }{ .{
.name = "Empty string",
.input = "",
.at = 0,
.expected = "",
}, .{
.name = "First char",
.input = "bar",
.at = 0,
.expected = "",
}, .{
.name = "Out of bound",
.input = "bar",
.at = 1000,
.expected = "ba",
} };
for (cases, 1..) |c, c_i| {
const total = cases.len;
const actual = try utils.String.lineBefore(c.at, c.input, alloc);
defer actual.deinit();
if (!std.mem.eql(u8, actual.items, c.expected)) {
std.debug.print("đź”´ {d}/{d} failed ({s})\n", .{ c_i, total, c.name });
std.debug.print(" ----\n", .{});
std.debug.print(" Input: '{s}'\n", .{c.input});
std.debug.print(" Expected: '{s}'\n", .{c.expected});
std.debug.print(" Actual: '{s}'\n", .{actual.items});
std.debug.print(" ----\n", .{});
return error.DoesNotMatch;
} else {
std.debug.print("🟢 {d}/{d} succeeded ({s})\n", .{ c_i, total, c.name });
}
}
}
In case of the latter, I can put that loop inside the test
declaration as well but then, it feels like I have to fight the default test runner’s output behaviour. Also, I looked at the test_runner.zig and of course hardly was able to understand something but I saw that it does additional checks like leaks in allocation when using std.testing.allocator
. However, under the hood it simply uses the gpa, which panics anyway in the debug build mode when I exit program without deallocating things properly.
Besides that struggle, I have some thoughts along the way. For example, is it like discouraged not using default test {}
suite? I haven’t yet reached the Zig build system but I saw the ability to add “addTest” step so I’m kind of worrying if I keep doing the dull “main()” way, I won’t be able to integrate it into the Zig’s building framework. Finally and more straightforwardly, if you’re a noob (like me), maybe it is better to stay safe within that “test {}” magic so that you won’t miss something you don’t understand yet.