Clarify the role of integration testing for code that requires I/O
This commit is contained in:
parent
d3c941a8c1
commit
ff76aaf525
1 changed files with 5 additions and 1 deletions
|
|
@ -59,7 +59,11 @@ When adding or modifying code, verify that tests cover the new logic. If coverag
|
||||||
|
|
||||||
**Pure I/O code is excluded from coverage requirements.** Code whose sole purpose is performing I/O (reading files, making network calls, rendering output) cannot be effectively tested without manual interaction. However, this has a direct design implication: keep the I/O layer as thin and trivial as possible. All business logic, validation, transformation, and decision-making must live in testable modules that the I/O layer merely calls into. A fat I/O layer is a design smell, not an excuse for missing tests.
|
**Pure I/O code is excluded from coverage requirements.** Code whose sole purpose is performing I/O (reading files, making network calls, rendering output) cannot be effectively tested without manual interaction. However, this has a direct design implication: keep the I/O layer as thin and trivial as possible. All business logic, validation, transformation, and decision-making must live in testable modules that the I/O layer merely calls into. A fat I/O layer is a design smell, not an excuse for missing tests.
|
||||||
|
|
||||||
**When business logic is tightly coupled to real I/O**, and separating them would genuinely undermine testability (not just convenience), prefer integration tests against an emulated service dedicated for testing. The emulated service must have reliable compatibility with the real target. For example, S3-dependent code can use MinIO via testcontainers or similar tooling to run tests that genuinely need I/O access. This approach is preferable to either mocking away the I/O (which hides real failure modes) or leaving the logic untested.
|
**The value of integration testing for I/O is context-dependent** — it depends on whether I/O is incidental to the component or central to its purpose.
|
||||||
|
|
||||||
|
When I/O is incidental (e.g., an application that loads configuration from a file), there is no value in testing the file-reading call itself — trust the language's I/O primitives. Instead, feed raw data to a pure function that handles parsing and validation. In some cases even parsing tests may be unnecessary, such as a JSON config file loaded via a standard-library routine that directly constructs application-defined structs. Structure such code to confine I/O in a short routine that can be excluded from coverage.
|
||||||
|
|
||||||
|
When I/O *is* the core business logic (e.g., a database engine or FUSE filesystem), it must be thoroughly integration-tested against a functioning backend. The I/O layer cannot be excluded here because it is the component's reason for existing. Provision appropriate test infrastructure: a tmpfs filesystem for storage-centric tests, an Alpine testcontainer for cases that need to exercise interactions between different user permissions, or an emulated service with reliable compatibility to the real target (e.g., MinIO via testcontainers for S3-dependent code). This is preferable to either mocking away the I/O (which hides real failure modes) or leaving the logic untested.
|
||||||
|
|
||||||
**Tests must exercise actual code paths, not reproduce them.** In rare cases, code is so trivial that the only apparent way to test it is to restate it in the test. Such tests verify nothing — they pass by construction and remain passing even when the code changes, which demonstrates that they provide no actual validation. Do not write these. Instead, explicitly exclude the code from coverage. Note that this situation is rare and usually signals a design gap (logic that should be extracted or combined with something more substantive) rather than inherent untestability.
|
**Tests must exercise actual code paths, not reproduce them.** In rare cases, code is so trivial that the only apparent way to test it is to restate it in the test. Such tests verify nothing — they pass by construction and remain passing even when the code changes, which demonstrates that they provide no actual validation. Do not write these. Instead, explicitly exclude the code from coverage. Note that this situation is rare and usually signals a design gap (logic that should be extracted or combined with something more substantive) rather than inherent untestability.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue