How to test self-dependent module?

Hi, I’m trying to write unit tests for my project that depends on itself:

./src
├── me.zig
├── mod_a.zig
├── mod_b.zig
└── test.zig

me.zig exposes mod_a.zig and mod_b.zig:

pub const mod_a = @import("mod_a.zig");
pub const mod_b = @import("mod_b.zig");

mod_a has a function and a test that calls a function from mod_b:

const mod_b = @import("me").mod_b;

pub fn mod_a_fn() void {
    @import("std").log.info("hello mod_a", .{});
    mod_b.mod_b_fn();
}

test "mod_a_test" {
    mod_b.mod_b_fn();
    try @import("std").testing.expect(true);
}

mod_b does not have any dependencies, so I don’t write here.

I added me as a module to my executable by addImport(), and it works completely fine for main executable:

    const me = b.createModule(.{
        .root_source_file = b.path("src/me.zig"),
    });
    const exe = b.addExecutable(.{
        .name = "main",
        .root_source_file = .{ .path = "src/main.zig" },
        .target = target,
        .optimize = optimize,
    });
    exe.root_module.addImport("me", me);
    me.addImport("me", me);

However, when I tried to add me module to my unit test, problem occurs.

Try 1: Import me module to the test executable

First, I tried to add me module to my test executable in the same way as a main executable. My build.zig is:

    const exe_unit_tests = b.addTest(.{
        .name = "unit",
        .root_source_file = .{ .path = "src/test.zig" },
        .target = target,
        .optimize = optimize,
    });
    exe_unit_tests.root_module.addImport("me", me);

test.zig is:

pub const me = @import("me");
comptime {
    @import("std").testing.refAllDeclsRecursive(@This());
    @import("std").testing.refAllDeclsRecursive(me);
}

In this case, no tests were run. I guess this is because Zig does not run tests in a dependent modules (though I want it to be tested).

Try 2: Add me module and import me.zig

I tried to import mod_a.zig or me.zig instead of me module in test.zig:

comptime {
    _ = @import("me.zig");
    @import("std").testing.refAllDeclsRecursive(@This());
}

However, the build fails saying that error: file exists in multiple modules. I know that this is because me.zig is imported twice in test.zig and root module (me itself).

Try 3: Do not add module me

Finally, I removed addImport() from the test executable and used relative imports. build.zig is:

    const exe_unit_tests = b.addTest(.{
        .name = "unit",
        .root_source_file = .{ .path = "src/test.zig" },
        .target = target,
        .optimize = optimize,
    });

test.zig is:

comptime {
    _ = @import("me.zig"); // or mod_a.zig
    @import("std").testing.refAllDeclsRecursive(@This());
}

It fails of course, because module me is not defined.

So all my tries resulted in failures. Could you tell me how to add unit tests to the project that imports itself?

Like you found out with Try 1, test declarations from imported modules are not added to the total list of test declarations to invoke. I suspect that this is a feature and not a bug because you probably don’t want to run a library’s tests when you are testing your own code.

What you need to test both your exe and the module is to build two test executables, one with main.zig as its root source file and another with me.zig, and make sure you include a root test declaration and/or refAllDeclsRecursive in a comptime block in both of those files.

const me = b.createModule(.{
    .root_source_file = b.path("src/me.zig"),
});
me.addImport("me", me);

const exe = b.addExecutable(.{
    .name = "main",
    .root_source_file = .{ .path = "src/main.zig" },
    .target = target,
    .optimize = optimize,
});
exe.root_module.addImport("me", me);

// Test everything that belongs to the same module as 'main.zig'
const exe_unit_tests = b.addTest(.{
    .name = "exe_unit",
    .root_source_file = .{ .path = "src/main.zig" },
    .target = target,
    .optimize = optimize,
});
exe_unit_tests.addImport("me", me);

// Test everything that belongs to the same module as 'me.zig'
const me_unit_tests = b.addTest(.{
    .name = "me_unit",
    .root_source_file = .{ .path = "src/me.zig" },
    .target = target,
    .optimize = optimize,
});
// An executable can add its root module as an import just like with any other module 
me_unit_tests.root_module.addImport("me", &me_unit_tests.root_module);

const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
const run_me_unit_tests = b.addRunArtifact(me_unit_tests);

const test_top_level_step = b.step("test", "test");
test_top_level_step.dependOn(&run_exe_unit_tests.step);
test_top_level_step.dependOn(&run_me_unit_tests.step);
2 Likes

Thanks for your reply, and it worked completely!

I didn’t know that I can add a module to itself by addImport("me", &me_unit_tests.root_module). (I tried to do that by addImport("me", me), but it resulted in a error of file exists in multiple modules).

I really appreciate it :slight_smile: