# Which command-line behaviors need subprocess tests instead of function tests?

> Test CLI logic in functions and use subprocess tests for exit codes, streams, signals, environment, and other process-boundary behavior.

Canonical URL: https://www.devobs.io/articles/qa-cli-subprocess-test-boundary/
By: Sofia Reyes
Published: 2023-11-06T23:28:21.017Z
Updated: 2026-09-06T08:31:04.426Z
Section: Architecture

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](https://docs.python.org/3/library/subprocess.html) defines that boundary in terms of spawning a new process, connecting pipes, and obtaining return codes. The [Node.js child_process documentation](https://nodejs.org/api/child_process.html) 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

## Source references

- <https://docs.python.org/3/library/subprocess.html>
- <https://nodejs.org/api/child_process.html>
