Recommendations for structuring project with library and unit tests

I’ve been learning Zig and am looking for best practices regarding how to structure projects, particularly with respect to where tests should go, and how the compiler infers which files are needed for compilation.

Let’s say I have a super simple project, where I am building a shared library, and some tests that verify its functionality. 8  Unit tests – An Introduction to Zig says the following on structuring tests:

If you look at the source code for most of the files present in the Zig Standard Library1, you can see that the test blocks are written together with the normal source code of the library.

Each programmer might have a different opinion on this. Some of them might prefer to keep unit tests separate from the actual source code of their application. If that is your case, you can simply create a separate tests folder in your project, and start writing Zig modules that contains only unit tests

While I can agree with keeping tests alongside the code itself, I personally would prefer to keep the tests separate. This is mainly because if a module file is already a significant number of lines, I don’t want to effectively double (or even more than double) that size by adding extra test code in there as well. I prefer to keep the concerns separate by moving the tests into a tests folder, but maybe people here could change my mind on that.

However, assuming that a separate folder is the option that I go for, this implies I should structure the project something like this:

build.zig
build.zig.zon
src/
  root.zig // Imports everything, including dummy module and tests
  dummy.zig // Some implementation I want to test
tests/
  all.zig // Imports all test modules, so root.zig only needs @import("tests/all.zig")
  dummy.zig // Tests for the implementation above

I have my build.zig set up like so (relevant parts only here):

const lib = b.addStaticLibrary(.{
  .name = "myproject",
  .root_source_file = b.path("src/root.zig"),
  .target = target,
  .optimize = optimize,
});

const lib_unit_tests = b.addTest(.{
  // Assuming this needs to be the same root file as the library,
  // since it's the library we're building tests for?
  .root_source_file = b.path("src/root.zig"),
  .target = target,
  .optimize = optimize,
});

const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);

const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_lib_unit_tests.step);

In theory, if root.zig is importing tests/all.zig, which is in turn importing tests/dummy.zig, then running zig build test should build and run the tests in tests/dummy.zig, as far as I understand. However, when I do run this command, nothing happens. Even with:

test "Dummy test" {
    std.testing.expectEqual(1, 0);
}

Nothing is reported after running zig build test.

What exactly is going wrong here? Am I misunderstanding how these files are intended to work together?

1 Like

Hi, welcome to the community!

  1. Keeping tests alongside code is more convenient for anyone reading your code since tests document functionality usage. Moreover, Zig has doctests.
  2. In order to run all tests in your setup you need to edit tests/all.zig to look like this:
test {
    _ = @import("dummy.zig");
}

and in your build.zig change tests’ root source file to tests/all.zig:

const lib_unit_tests = b.addTest(.{
  .root_source_file = b.path("tests/all.zig"),
  .target = target,
  .optimize = optimize,
});

Thanks, this did work! The only thing was that I had to add the lib target to lib_unit_tests, as I wasn’t allowed to import files outside the unit test target’s root directory. For the reference of anyone else, this was achieved using:

lib_unit_tests.root_module.addImport("myproject", &lib.root_module);

I also had to remove the reference to the tests from the actual library’s root file, as there was no longer a need for them there given that file is not the one that’s built by the unit test target any more.

I will consider putting tests directly into the source files of individual modules where it makes sense to do so, but it’s definitely good to have the option to place certain tests in a separate directory tree if they might otherwise clutter the main source.

1 Like