Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

73 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

cmdtest

CLI testing for Zig.

Installation

  1. Fetch the latest release:

    zig fetch --save=cmdtest https://github.com/pyk/cmdtest/archive/v0.3.0.tar.gz

    This updates build.zig.zon.

  2. 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(testing.io, .{ .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(testing.io, .{ .argv = argv });
         defer result.deinit();
    
         try testing.expectEqualStrings("project-exe\n", result.stderr);
     }
  3. 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);
     }
  4. Run the tests:

    zig build test --summary all

See minimal Zig project cmdtest-example.

Usage

There are only 2 functions:

  • add(b, options) registers a CLI test step in build.zig. zig build test will run the tests.
  • run(io, options) spawns a child process, captures stdout and stderr (up to a limit), waits for the child to finish, and returns a RunResult.

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(testing.io, .{ .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(testing.io, .{ .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(testing.io, .{
  .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 stderr output.
  • 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:

  • run uses testing.allocator by default.
  • You can pass a different allocator in RunOptions.allocator to control where captured buffers are allocated.

Development

Install the Zig toolchain via mise (optional):

mise trust
mise install

Run tests:

make test

Build library:

make build

License

See LICENSE.

Releases

Contributors

Languages