Use subprocess tests for behaviors that only exist at the operating-system process boundary: exit status, stdout versus stderr, stdin handling, signal termination, working-directory resolution, environment-sensitive command lookup, and timeout or pipe-buffer behavior. Keep parsing helpers, validation rules, and data transformations in ordinary function tests. That split gives you fast coverage for business logic and a smaller set of higher-value tests for what a real CLI caller actually observes.
What belongs in function tests, and what belongs in subprocess tests?
If the code would behave the same when called as a library function, test it as a function. That includes parsing helpers after argument decoding, validation, transformations, and output formatting logic.
Use subprocess tests when the contract depends on a real child process. The Python subprocess documentation defines that boundary in terms of spawning a new process, connecting pipes, and obtaining return codes. The Node.js child_process documentation likewise describes real stdin, stdout, and stderr pipes plus environment-based command lookup. A function call cannot prove those behaviors.
Which CLI behaviors specifically require subprocess coverage?
Start with exit codes. A raised exception inside your program is not the same contract as a process returning 0, 1, or another documented code. In Python, CompletedProcess.returncode is the externally visible result, and negative values indicate termination by signal on POSIX.
Then test stream separation. If your tool promises machine-readable output on stdout and diagnostics on stderr, only a subprocess test can verify that split. Python documents capture_output=True and also how to intentionally combine streams with stderr=STDOUT. Node documents separate pipes by default, which is exactly the observable behavior shell users rely on.
Test stdin through a subprocess whenever the CLI reads piped data, prompt input, or EOF-sensitive input. Python run(..., input=...) sends data to the child process stdin, making it practical to verify newline handling and non-interactive behavior.
When do cwd, environment, and signals matter enough to test at the boundary?
Use subprocess tests for any behavior that depends on process setup rather than pure logic. That includes relative path resolution, config discovery from the current directory, and writing output into a cwd chosen by the caller. Python exposes cwd and env directly on run(), which is a strong hint these are process-contract concerns.
Environment handling also belongs here. Node states that command lookup uses options.env.PATH when provided, with Windows-specific caveats around case-insensitive environment keys. If your CLI changes behavior based on PATH, locale, or feature flags, test the ordered boundary that actually enforces it: parent process starts child with a specific environment, child resolves commands under that environment.
Signals and timeouts are the other clear boundary case. Python documents that when a timeout expires, the child process is killed and waited for. Node documents child exit events and signal state. Those lifecycle behaviors do not exist in a function test.
What is a practical checklist for a CLI suite?
Use one subprocess test per external contract, not per business rule:
- success returns the documented exit code
- invalid input returns a non-zero exit code
- stdout contains user or machine output
- stderr contains diagnostics only
- piped stdin is consumed correctly
- relative paths are resolved from the chosen cwd
- required environment variables are honored
- long-running commands terminate correctly on timeout or signal
Worked example: for report generate, keep report computation in function tests. Add subprocess tests for --format json writing JSON to stdout, validation failures going to stderr, reading input from stdin, and creating ./out/report.json relative to the subprocess cwd.
How should interactive prompts be tested?
Test the prompt loop with a subprocess if you care about the actual terminal-facing contract: where the prompt is written, what happens on EOF, and which exit code follows cancellation. Keep the decision logic behind the prompt in function tests.
Which signal tests should be platform-specific?
Only test signals your deployment environment actually uses, and scope them by platform. POSIX signal return-code behavior does not map cleanly to Windows.
Next step: review your CLI test suite and move only user-visible process contracts into subprocess tests; keep all reusable business rules in function tests.
Reviewed: 2026-09-05
Sources are linked throughout this guide. Product capabilities can change; consult the linked documentation for your deployment.
Read our editorial approach ↗