Quality Assurance 04 Testability

Posted by LiYixian on Wednesday, October 29, 2025 | 阅读 | ,阅读约 3 分钟

What is Testability?

Testability is the extent to which a system’s behaviors can be exercised, observed, and reasoned about with fast, reliable tests.

  • Controllability
    • steer the system under test into any path or behavior
    • How difficult is it to put the system into a particular state? How to write a test that covers that behavior?
  • Observability
    • directly observe behaviors and check outcomes
    • How hard is it to determine if the system behaved as intended? Can we check that the system didn’t misbehave?
  • Isolation
    • exercise the component under test without exercising the rest of the system
  • Stability
    • produce the same test result across runs
    • Is the result independent of time, randomness, order, and load?

Writing Testable Code

Testability InhibitorsPhenomenonRefactor
Hidden Dependencies- Environment Variables
- Platform path rules
- Current working directory
- Ports and permissions
- Time zone / locale (machine dependent date handling)
- Machine sizing (CPU count) affecting behavior
- Filesystem permissions and layout
Make dependencies explicit via configuration
Poor Isolation- Hardwired collaborators (direct instantations / static methods / singletons / service locators)
- Deep method chains
Dependency Injection
Busy Constructors- Side Effects (e.g., IO, timers, network, background jobs)
- Contain complex logic
Move logic out of the constructor
Non-Determinism- Hidden clock (date / time)
- Randomness
- External API calls
- Networking
- Parallelism
- Race condition (no await)
Add deterministic seams via dependency injection
Poor Observability- Void functions with difficult to observe side effects
- Ambiguous returns (e.g., true/false → what succeeded or failed?)
- Hidden state (e.g., private state without getters)
Provide read access to important internal state;
Consider using informative return types;
Add observability hooks to avoid test doubles
Shared Mutable State- Globals, statics, singletons
- Mutable configurations
- Process-wide settings
- Exposed internals
Try to avoid shared mutable state
External Libraries and Legacy Code- Application talks directly to external libraries or external codeWrap external libraries and legacy code in wrappers and test via dependency injection
Too Much Responsibility- One class/function does orchestration, domain logic and I/OSplit into functions with a single responsibility

Writing Good Test Code

Test code should be …

  • Simple: minimal logic; clear Arrange–Act–Assert.
  • Readable: names tell the story; intent over mechanics.
  • Maintainable: low coupling; stable helpers; clear failure messages
  • DRY (not redundant): share setup wisely without hiding intent.
  • Deterministic: controls time, randomness, and concurrency.
  • Fast: no real I/O or sleeps; runs in milliseconds.
  • Focused: verifies one behavior so failures point to a single cause.
    • Use table-driven testing for multiple inputs
  • Independent: order-agnostic; no shared state; dependencies faked.