Skip to content

What to Test and What Not To

When to Use

When planning test coverage for a new feature, when reviewing existing tests for ROI, or when feeling pressure to test everything. Deciding what not to test is as important as deciding what to test.

Test Behavior, Not Implementation

The most reusable test principle: test what a unit does (observable behavior), not how it does it (internal implementation).

A test that verifies observable behavior survives refactoring. A test that verifies internal method calls, field names, or execution sequence breaks every time you improve the code.

# BAD: tests implementation (how the discount is stored internally)
def test_vip_discount_sets_discount_flag():
    cart = Cart()
    cart.apply_vip()
    assert cart._discount_rate == 0.10  # testing private field

# GOOD: tests behavior (what the customer observes)
def test_vip_discount_reduces_total_by_10_percent():
    cart = Cart(items=[Item(price=100)])
    cart.apply_vip()
    assert cart.total() == 90.00  # testing observable outcome

When _discount_rate is renamed, the bad test breaks. When the implementation is replaced with a lookup table, the bad test breaks. The good test survives both changes.

What to Test: Risk/ROI Matrix

Code type Risk Test investment Why
Business logic (rules, calculations, state machines) High Unit tests, parameterized edge cases Bugs here lose money or trust
Auth and permissions High Integration + functional Silent bugs in auth are security vulnerabilities
Data transformations and parsing High Unit, table-driven Edge cases and encoding errors are hard to spot
API boundaries (request/response) High Integration at the HTTP layer Serialization bugs reach all consumers
Critical user journeys (checkout, login) High E2E smoke test + integration Must work end-to-end for the app to function
UI component interactions Medium Integration (Testing Library style) Most UI bugs are in component composition, not isolation
Simple CRUD with no business logic Low One integration test to verify wiring Logic is trivial; wiring test confirms it is hooked up
Configuration and defaults Low One integration test Just verify it applies
Third-party library behavior None Do not test it Trust the library; test your usage of it
Generated code (ORMs, scaffolding) None Do not test it The generator should be tested, not its output
Trivial getters/setters None Do not test them No logic to break

The Testing Library Principle

Kent C. Dodds / Testing Library: "The more your tests resemble the way your software is used, the more confidence they can give you."

This principle guides what to test at each level: - Test user-visible behavior, not internal state - Test from the outside (HTTP response, DOM output, return value) rather than from inside (private field, internal method) - Tests that break when behavior is unchanged and only internal structure changed are testing the wrong thing

What Not to Test

Framework and library internals — Drupal's render pipeline, React's reconciler, ORM query generation. Test that your configuration produces the expected output, not that the framework does its job.

Trivial delegation — A method that only calls another method with the same arguments provides no logic to test. Test the delegated method directly.

Private methods — Private methods are implementation details. Test them through the public API they support. If a private method is complex enough to warrant direct testing, it probably should be extracted to its own class.

Code written to make a test pass — If you wrote helper code purely to satisfy a test (no production use), delete the helper.

Assertions that can never fail — A test that asserts assert True or assert response is not None where response can never be None given the Arrange phase adds noise with no value.

Common Mistakes

  • Testing private methods directly → Couples tests to implementation; breaks on any refactor regardless of behavior
  • Testing third-party libraries → Wastes effort; if the library is broken, its own tests should catch it; test your usage
  • Missing edge cases in favor of happy-path only → Most bugs live at boundaries (null, empty, negative, overflow); always add edge case tests for logic-heavy code
  • Testing that mocks return the right value → You configured the mock; it will return what you told it; this tests nothing
  • Over-testing trivial code to hit coverage targets → Reduces signal-to-noise in the test suite; lowers ROI of every test

See Also