I am a new to Zig and was intrigued by the idea of what comptime could do for generating code. The first idea I wanted to try was using comptime for loops to generate a bunch of orthogonal test cases. That didn’t quite work as expected (compile error: “expected statement, found test”) until I found this forum solution: Generating Tests at Comptime - #4 by koe
But, the solution does not provide a way to name the test, so this really isn’t a full solution (I want to test a subset of tests often). I was curious if anyone else could point a way to also naming tests.
That post also didn’t really explain why the solution worked, I’m still curious…why doesn’t something like this work? Fails with “expected statement, found test”
comptime {
for (.{ "alpha", "beta" }) |kind| {
for (.{ 1, 2 }) |part| {
test std.fmt.comptimePrint("test_{s}_part{d}", .{ kind, part }) {
try test_helper(kind,part);
}
}
}
}
Tests can only exist at the top level of a file or type, so you can just wrap it in a struct.
But it still won’t work because test does not expect an expression that evaluate to a string (slice of bytes).
test expects either a string literal, or the name of a declaration (not in quotes). The latter will be included as an example for the specified declaration in generated docs.
and the trace shows that the first test (not comptime) works as expected, but the generated test name is not the string literal, but the anonymous struct plus the test_name var rather than the string value. Any ideas on getting that string value to work?
$ zig test ./main.zig
1/5 main.test.bare_test...FAIL (TestUnexpectedResult)
/usr/local/zig/lib/std/testing.zig:607:14: 0x102c019 in expect (std.zig)
if (!ok) return error.TestUnexpectedResult;
^
/workspaces/advent_of_code_2025/Rog/main.zig:107:5: 0x102c0ab in test_helper (main.zig)
try std.testing.expect(false);
^
/workspaces/advent_of_code_2025/Rog/main.zig:110:5: 0x102c136 in test.bare_test (main.zig)
try test_helper("bare", 0);
^
2/5 main.comptime__struct_17749.decltest.test_name...FAIL (TestUnexpectedResult)...
/usr/local/zig/lib/std/testing.zig:607:14: 0x102c019 in expect (std.zig)
if (!ok) return error.TestUnexpectedResult;
^
/workspaces/advent_of_code_2025/Rog/main.zig:107:5: 0x102c0ab in test_helper (main.zig)
try std.testing.expect(false);
^
/workspaces/advent_of_code_2025/Rog/main.zig:119:21: 0x11bae09 in decltest.test_name (main.zig)
try test_helper(kind, part);
^
3/5 main.comptime__struct_17750.decltest.test_name...FAIL (TestUnexpectedResult)
But to clarify: test decl_name does not name the test with the value of decl_name, rather it is testing decl_name and will be used as an example in docs.
This comptime__struct_154 comes from creating an unnamed struct in a comptime block.
Right, you’re making it clear to me that comptime cannot produce a string literal. I was confused thinking that it could…but I’m getting now that of course string literals are created prior to compiling.
Original poster for that solution. Yes it is incomplete, I would love for a way to do this though if feasible, for me a use case comes up quite frequently. Its quite nice to have seperate tests e.g. per impl. Maybe a language change would be required though if my reading of this thread is correct.
const impls = .{Foo, Bar, Baz};
test impls {
for (impls) |impl| {
impl.do(123);
}
}
if its too tedious/you cant access all the implementations from where the tests are, you can have a testImpl function that you can import and use where each impl is.
You can make a name appear as part of the name of a parent namespace, by making that parent namespace into a generic which takes that name as a parameter, I would only use this if the set of names can’t be enumerated by hand, for example (maybe) in this case (if the file names were defined via a build option or similar):
Here I would suggest to just write it out:
test "alpha_1" {
try test_helper("alpha", 1);
}
test "alpha_2" {
try test_helper("alpha", 2);
}
test "beta_1" {
try test_helper("beta", 1);
}
test "beta_2" {
try test_helper("beta", 2);
}
// or
test "alpha/beta" {
try test_helper("alpha", 1);
try test_helper("alpha", 2);
try test_helper("beta", 1);
try test_helper("beta", 2);
}
I don’t think Zig needs to be used in a (often) Lisp like manner, where you write a macro for every single repeating thing, even in Lisp it can be annoying when people go overboard with trying to remove any kind of repetition.
If your actual use-case is more complicated than what you gave as example, then my answer might be different.
Sometimes it also may make sense to write a generator, which writes it out which is used via the buildsystem (So it uses code generation instead of comptime). I think if you need to generate many such tests then code generation could be quite a bit better.
Here is a revised version and with that, I am starting to like it (although I still prefer writing it manually for simple cases / where possible):
const std = @import("std");
pub fn Test(comptime args: anytype) type {
return struct {
const reference = args;
pub fn Impl(Def: type) type {
return struct {
const Self = @This();
test Self {
try Def.impl();
}
};
}
};
}
// This can be used in the args value so that the string value actually gets printed
// but using enum literals instead is better because those get printed more reliably
pub fn Literal(comptime str: []const u8) type {
return opaque {
const ref = str;
};
}
comptime {
const files = .{ "hay", "hey", "hooi", "needle" };
for (files) |file| {
_ = Test(file).Impl(struct {
fn impl() !void {
try std.testing.expect(std.mem.eql(u8, file, "needle"));
}
});
}
}
comptime {
for (.{ .alpha, .beta }) |kind| {
for (.{ 1, 2 }) |part| {
_ = Test(.{ .kind = kind, .part = part }).Impl(struct {
fn impl() !void {
try test_helper(@tagName(kind), part);
}
});
}
}
}
fn test_helper(kind: []const u8, part: i32) !void {
std.log.debug("test_helper({s}, {d})", .{ kind, part });
try std.testing.expect(false);
}
Some tips:
use enum literals instead of strings and then convert them to strings when needed
(because the enum literals are printed when part of a compound args value, while the strings are elided)
Literal can be used to force that a string value will show up in the printing of a compound args
Zigs idiomatic approach for this is different, you just write all the tests giving them different names manually, then you specify a test filter to run the tests that match the filter.
Test Options:
–test-filter [text] Skip tests that do not match any filter
Or .filters in the test options if you are using b.addTest in a build.zig.
I have a collection of types A, B, C, … that satisfy an interface X. I want to test to satisfy that interface X is satisfied with a common test. Then, if I mess up any implementation, it would be nice if “zig build test” told me e.g.: “‘impl B functionality X’ failed”. This provides me a pleasant development experience with minimal clutter.
Of course, you can put them all in one test too, but theres a little bit more mental parsing to do for the developer on test failure (inferior dev experience). Honestly this is kind of a minor thing, nice to have in view
The implementation provided by sze is a nice step in that direction though! I appreciate him sharing that.
EDIT: moreover, if we group all tests in one big one, we stop at the first failure. This omits information like what tests could succeed, and which other ones would have failed (if I understand correctly).
You can get this behavior by running your tests through the build system. This introduces another process into the mix (the build runner process) which then can restart the test runner process when a failure occurs. This also provides the feature of timing out unit tests that take too long:
--test-timeout <timeout> Limit execution time of unit tests, terminating if exceeded.
The timeout must include a unit: ns, us, ms, s, m, h
I would make a function that tests an implementation, then the implementations can have their own test call the function.
This has a couple of good properties
the implementations don’t need to be accessible where the “test”(function) is. though the opposite is now required.
different implementations can have wildly different setups, so you will need to write the boilerplate to test it at all. This separates all that boilerplate so it’s easier for a human to parse.
you can expose your test function to allow third parties to test their own implementations.
My reading of this is partially that Zig intends to some extent limit the expressivity of comptime, limiting possibilities like this. Not criticism, I don’t have a qualified opinion on this, but is this correct?
Regardless if you’ll indulge me, my dream API as a comptime exploiter is the possibility to generate everything about a struct at comptime: functions, fields and also tests.