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.)