CLI testing for Zig.
-
Fetch the latest release:
zig fetch --save=cmdtest https://github.com/pyk/cmdtest/archive/v0.2.0.tar.gz
This updates
build.zig.zon. -
Write your test file. Example:
test/mycli.zig.const std = @import("std"); const cmdtest = @import("cmdtest"); const testing = std.testing; test "via exe name" { const argv = &[_][]const u8{"mycli"}; var result = try cmdtest.run(.{ .argv = argv }); defer result.deinit(); try testing.expectEqualStrings("project-exe\n", result.stderr); } test "via path" { const argv = &[_][]const u8{"./zig-out/bin/mycli"}; var result = try cmdtest.run(.{ .argv = argv }); defer result.deinit(); try testing.expectEqualStrings("project-exe\n", result.stderr); }
-
Register the test in
build.zig:const std = @import("std"); const cmdtest = @import("cmdtest"); pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); // Your CLI const cli = b.addExecutable(.{ .name = "mycli", .root_module = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, }), }); b.installArtifact(cli); // Register new test const cli_test = cmdtest.add(b, .{ .name = "mycli", .test_file = b.path("test/mycli.zig"), }); const test_step = b.step("test", "Run tests"); test_step.dependOn(&cli_test.step); }
-
Run the tests:
zig build test --summary all
See minimal Zig project cmdtest-example.
There are only 2 functions:
add(b, options)registers a CLI test step inbuild.zig.zig build testwill run the tests.run(options)spawns a child process, captures stdout and stderr (up to a limit), waits for the child to finish, and returns aRunResult.
Basic run and stdout assertion:
const std = @import("std");
const cmdtest = @import("cmdtest");
const testing = std.testing;
test "echo" {
const argv = &[_][]const u8{"echo", "hello"};
var result = try cmdtest.run(.{ .argv = argv });
defer result.deinit();
try testing.expectEqualStrings("hello\n", result.stdout);
}Write to stdin and capture stdout:
const std = @import("std");
const cmdtest = @import("cmdtest");
const testing = std.testing;
test "cat" {
const argv = &[_][]const u8{"cat"};
const input = "a\nb\n";
var result = try cmdtest.run(.{ .argv = argv, .stdin = input });
defer result.deinit();
try testing.expectEqualStrings(input, result.stdout);
}Limit how many stdin bytes are sent:
const payload = large_slice; // some []const u8
var result = try cmdtest.run(.{
.argv = argv,
.stdin = payload,
.max_stdin_bytes = 1024, // truncate to 1 KiB
});
defer result.deinit();And you can do much more:
- Automatically discover built artifacts.
- Capture
stderroutput. - Limit how much output is captured.
- Run with a custom working directory.
- Run with a custom environment map.
- Handle exit status and signals.
- Detect spawn errors.
See src/test_exe.zig.
Some additional notes about the usage of allocator:
runusestesting.allocatorby default.- You can pass a different allocator in
RunOptions.allocatorto control where captured buffers are allocated.
Install the Zig toolchain via mise (optional):
mise trust
mise installRun tests:
zig build test --summary allBuild library:
zig buildSee LICENSE.