Software Testing Best Practices

Chapter 1 · Practice 3 of 4

Test observable behaviour through the public interface

From Choosing what to test and at which level

Why it matters

A test should describe what the code does for its callers, not how it does it. Tests that reach into private methods, assert on internal data structures or check the exact order of internal calls break every time you refactor, even when the behaviour is unchanged. Those tests slow change down instead of making it safe.

Fowler's article is direct about private methods: they are implementation details. If one seems to need its own test, the class probably does too much and the logic should move into its own unit with a public interface.

The same idea applies in the browser. Testing Library's guiding principle is that a test should resemble how users interact with your code. Query by role, label and visible text, not by CSS class or component internals.

A good check: could you rewrite the internals of this function and keep the test green? If not, the test is coupled to the implementation.

Avoid

  • Exporting a function only so a test can call it
  • Asserting on private fields or internal state

Example

TypeScript
// Coupled to internals: breaks if the cache becomes a Map or moves
expect(service['cache'].length).toBe(1);

// Tests behaviour: the second call does not hit the API again
await service.getUser('42');
await service.getUser('42');
expect(api.fetchUser).toHaveBeenCalledTimes(1);