How to import a file in tests?

I have a the following directory structure:

project
  |-> src
    |-> main.zig
    |-> stack.zig
    |-> unicit
      |-> prolly.zig

Inside of prolly.zig, I have

const stack = @import("../stack.zig");

I found that when running zig build run, I need to add the following to both lib and exec in build.zig:

lib.addIncludePath(b.path("src"));
...some exec stuff
exe.addIncludePath(b.path("src"));

So then zig build run works.

But now, when I run zig test ./src/unicit/prolly.zig, it doesn’t work. It is confused about where stack.zig is again.

This is also still the case when I run zig test ./src/unicit/prolly.zig -I src

I also tried:

    const prolly_tests = b.addTest(.{
        .root_source_file = b.path("src/unicit/prolly.zig"),
        .target = target,
        .optimize = optimize,
    });
    prolly_tests.addIncludePath(b.path("src")); // Add include path for tests
    const run_prolly_tests = b.addRunArtifact(prolly_tests);

It’s a no go.

➜  database git:(main) ✗ zig build test
test
└─ run test
   └─ zig test Debug native 1 errors
src/unicit/prolly.zig:2:23: error: import of file outside module path: '../stack.zig'
const Stack = @import("../stack.zig");
                      ^~~~~~~~~~~~~~
referenced by:
    reverseKeyPath: src/unicit/prolly.zig:205:25
    test.Can find the reverse key path: src/unicit/prolly.zig:338:28
    remaining reference traces hidden; use '-freference-trace' to see all reference traces
error: the following command failed with 1 compilation errors:
/Users/iamwil/.zvm/0.13.0/zig test -ODebug -I /Users/iamwil/projects/code/fruitful_town/database/src -Mroot=/Users/iamwil/projects/code/fruitful_town/database/src/unicit/prolly.zig --cache-dir /Users/iamwil/projects/code/fruitful_town/database/.zig-cache --global-cache-dir /Users/iamwil/.cache/zig --name test --listen=- 
Build Summary: 4/7 steps succeeded; 1 failed (disable with --summary none)
test transitive failure
└─ run test transitive failure
   └─ zig test Debug native 1 errors
error: the following build command failed with exit code 1:
/Users/iamwil/projects/code/fruitful_town/database/.zig-cache/o/c8414a20a07487128b802b1c006e99c6/build /Users/iamwil/.zvm/0.13.0/zig /Users/iamwil/projects/code/fruitful_town/database /Users/iamwil/projects/code/fruitful_town/database/.zig-cache /Users/iamwil/.cache/zig --seed 0xb8c2159e -Z538cd959a42d4f27 test

What should I do so that I can use zig test? Or should I use zig build test? I’m not sure what the difference is, other than there’s a test runner with the former.

The typical way to handle this sort of thing is to have a “root” file that imports and references everything you want to test. For example:

src/root.zig

const stack = @import("stack.zig");
const prolly = @import("unicit/prolly.zig");

test {
    _ = stack;
    _ = prolly;
}

Then doing:

    const tests = b.addTest(.{
        .root_source_file = b.path("src/root.zig"),
        .target = target,
        .optimize = optimize,
    });
    const run_tests = b.addRunArtifact(tests);

would run the tests in root.zig, stack.zig, and unicit/prolly.zig.

Most of the time this root.zig file (sometimes also called lib.zig, or <yourlibname>.zig) will also be used as the root source file when using your module as a library, in which case, by using pub decls, it allows you to use the std.testing.refAllDecls helper like so:

const std = @import("std");

pub const stack = @import("stack.zig");
pub const prolly = @import("unicit/prolly.zig");

test {
    std.testing.refAllDecls(@This());
}

Side note:

addIncludePath is only for interacting with C code, so from what you’ve provided, there’s no need for it.

3 Likes

I am facing the similar issue reported by OP but a draw back of this suggestion is that it prevents you from being able to test a particular file singly by running zig test path/to/file

Plus I find the requirement that leads to the “import of file outside module path” error a bit frustrating, because it means I can’t easily move my files around without having to update the relative import path.

Question Is, is there a way to have an absolute import path to other files in my codebase? So that I don;t have to update the import path if I move files around?

1 Like

As discussed in a related thread, zig test isn’t integrated with the build system. As things stand, I suggest doing all things Zig through zig build.

Not exactly. What does work is making your file into a module in the build system, then importing it.

Say you want test code in ./test/test-code.zig and it needs a module rooted in ./src/some-module.zig. You can set this up like so in build.zig:

const some_module = b.addModule("some-module", .{
    .root_source_file = b.path("src/some-module.zig"),
    // ...
};

const unit_tests = b.addTest(.{
    .root_source_file = b.path("test/test-code.zig"),
    // ...
};

unit_tests.root_module.addImport("some-module", some-module);

Then test-code.zig can call @import("some-module"). If you end up moving things around, you have to update the .root_source_file, but everything else should keep working.

This isn’t specific to tests, it works for any case where you want to organize code into parallel directories. If you need to reach into an entire parallel directory/repo, and don’t want to play host-and-fetch right away, you can do that with build.zig.zon as well:

.dependencies = .{
    .other-repo = .{ 
        .path = "../in-parallel",
    },
},

You wouldn’t want to distribute code like this, but for local work it’s fine, and swapping out a .url and .hash when it’s time is easy.

This is a bit more work up front than a .. path, but scales better, for instance the module can be used in another project with zig fetch or using a .path in the build.zig.zon for local development.

Something to watch out for is that you can’t import two modules which both import the same file by path. That can always be solved by making the mutual dependency a module in the above fashion, among other ways, it’s just something to be aware of.

1 Like