Why discovery testing helps component-based UI development
Last week we took a look at two schools of test-driven development (TDD): the classical or Detroit school, which involves an inside-out approach to code design; and mockist or London school, which takes an outside-in approach, starting from expected behaviour and working through an implementation to match the required behaviour.
Justin Searls, who has written and presented extensively on TDD in the context of web applications, generally favours the London school approach, but has suggested his own extension of it, which he calls discovery testing.
Here’s the workflow described in Justin’s introduction to discovery testing:
- Start by identifying an entry point and writing a collaboration test of it
- For each dependency the first collaboration test identifies:
- if it needs to be broken down further, write another collaboration test for it (e.g. GOTO 1)
- if its task is a straightforward data transformation, implement it as a pure-function leaf node
- if its task requires interaction with a third-party, implement a wrapper object
It’s well worth taking a look at Justin’s example of discovery testing in practice. Even if you don’t like the approach, it’s interesting to see how other developers go about their work.
Steps 3-5 identify three archetypes of unit (functions, classes, etc) that are usually created during discovery testing:
- Collaborator objects - these are responsible for orchestrating or composing two or more other objects. These dependencies are mocked in unit tests.
- Pure function leaf nodes - these have no internal dependencies and no side effects
- Wrapper objects - Anything that requires third party interaction, I/O or other side effects (network access, user input, timeouts, etc). These dependencies are mocked in unit tests.
The key point here is that as much logic as possible is pushed down to the leaf nodes, where it can be tested in isolation without the need for mocks. Collaborator and wrapper objects are kept as simple as possible to keep test doubles to a minimum.
I enjoy the process of discovery testing, not because it makes me feel smug or clever, as many anti-TDDers might imagine, but because:
- It reduces the cognitive workload and uncertainty during development, because:
- It provides concrete heuristics for breaking down complexity in modern front-end web apps
It took me a while to realise that discovery testing works really well with modern component-based UI development (in React, Angular, Vue, etc). For all these frameworks, we are often encouraged to separate our components and modules into different concerns, similar to the three archetypes above.
For example, React and Redux applications often contain components and modules that are broken down into similar collaborator, wrapper and functional archetypes as above.
This makes it explicit what kind of unit tests we should be writing for each component, and encourages us to separate UI logic from orchestration from asynchronous side effects. Mixing these concerns together is a big source of pain in maintaining component-based JavaScript applications.
More to the point, it makes using TDD simpler, because we know what kind of object we’re creating at any given time, and we have rules for when we should be breaking them out into separate components or modules.
All the best,
– Jim