Questions on CLI application (unit testing and design)

Conditionally define ArgIterator as different types depending on the build mode:

pub const ArgIterator = if (@import("builtin").is_test)
    std.process.ArgIteratorGeneral
else
    std.process.ArgIterator;

pub fn parse(self: *Args, gpa: Allocator, out: Io.Writer, iter: *ArgIterator) !Args;

Or, use anytype duck typing:

pub fn parse(
    self: *Args,
    gpa: Allocator,
    out: Io.Writer,
    /// Should be `*std.process.ArgIterator` or something with an equivalent API.
    iter: anytype,
) !Args;

Either approach will let you write unit tests that mock OS functionality like argv and stderr. I would recommend the latter anytype approach because it provides the greatest flexibility for API consumers and lets them e.g. use ArgIteratorGeneral for parsing response files (files with command-line arguments, to get around command-line length limitations).

For testing against the real OS, this post has details on how how to write integration tests that test what is written to stdout/stderr:

(Note that it’s from January 2024, so the exact APIs might have changed a bit.)

2 Likes