zig-debugging-tests(1)

glfmn.io zig-debugging-tests(1)
Name

zig-debugging-tests

Some patterns I have started to use when writing zig code with unit tests.

Introduction

Although I try to make good use of the debugger, I am quite used to print-based debugging, especially for unit tests. However, one big problem with using debug print for debugging, is that the debug printing sometimes generates a lot of noise. If I am running something in a loop, and only one iteration of the loop produces the error, every iteration produces the full set of output.

Thanks to Zig's errdefer, I found a neat trick that really helps with this!

errdefer in tests

Zig using errors, instead of panics like rust unit tests, allows the programmer to use errdefer to print something only when a test actually fails.

While the std.testing.expect family of functions do a decent job of printing the output, sometimes I just need more context. For example, when testing a gap buffer, I might want to see the contents of the gap buffer at the failing test, and some extra stats about the gap buffer.

Instead of printing the gap buffer manually at various interesting points, I find that doing:

errdefer std.debug.print("{}", .{gap_buffer});

Does a fantastic job of avoiding cluttering the code.

Running tests in the debugger

If anything more complex is necessary, using the debugger is the way to go. However, naively trying to run seergdb or gdb -tui directly from the terminal gets difficult, as the test binaries are not in zig-out directory (understandably) but in the zig-cache directory.

I learned a trick from ziggit that the build.zig file can run commands, and you can feed the artifact path of a command as an argument into the command:

build.zig
// seergdb is a gdb gui frontend
const debugger = b.addSystemCommand(&.{ "seergdb", "--run", "--" });
debugger.addArtifactArg(exe_unit_tests);

const debug_step = b.step("debug", "Run unit tests under debugger");
debug_step.dependOn(&debugger.step);

This makes it really easy to run the proper binary; however, this won't always stop execution for test errors, so it may necessitate adding some @breakpoint calls.