11 Data Oriented Testing
The chapter presents testing as a data problem: good tests should make the relationship between inputs and assertions obvious, keep relevant state close to the test, and avoid burying meaning in shared setup or opaque mocks. It contrasts simple, deterministic functions with more conventional object-oriented code, showing that data-oriented design makes testing easier because the behavior is expressed as clear inputs and outputs. When code mixes side effects with logic, the chapter recommends splitting it into a pure core and an imperative shell so the meaningful behavior can still be tested in isolation.
A major theme is that tests become much stronger when they are driven by data rather than by hard-coded examples. By representing cases explicitly, deriving expected results from the test data, and using helpers that highlight only the relevant attributes, tests become self-describing and more resistant to change. The chapter also encourages using data transformations, power sets, and other computed combinations to explore more scenarios than a hand-written suite would normally cover, including edge cases such as empty inputs or boundary values. This approach can expose bugs that fixed examples might miss and makes assertions more trustworthy even when they are relatively simple.
The chapter’s second half focuses on randomized test data as a way to widen coverage, reduce accidental coupling, and improve confidence in properties that should hold across many states. Randomization is used to generate varied inputs, create repeatable streams of examples, and test broad invariants such as round-tripping, associativity, or threshold-based behavior across many combinations. It emphasizes that randomization is a tool, not a universal solution: some tests are better as small deterministic examples, especially when the state space is already exhaustively represented. The chapter closes by arguing that test quality comes from data coverage, good modeling, and clear narrative comments—not from chasing raw code coverage alone.
Organizing tests around data shows how inputs relate to assertions
A service with only side-effects
Coupling begins innocently
Shared test data in action
The collapse
Building upon a base of randomized data makes reuse and sharing safe
There’s a giant set of states we’ve yet to explore
The denotative view of the world. Every data type holds a set of states we should explore
When we visualize set of states the test exercises, we find that it is very few!
Combining multiple specific subsets into one primary set representing “all” states
Carving out a subset of data while constructing a test
The commonly shown testing pyramid.
Summary
- Unit and integration tests each fill different needs and solve different problems
- Data Oriented code is inherently testable. You only need inputs and outputs.
- Tests can (and should!) be data oriented even if the code they’re testing is not
- Use data to show what’s important in the test
- Everything relevant to the test should be in the test, not hidden outside it
- Understanding a test requires a clear link between inputs and assertions
- Structure your test as a consumer of data. This allows the same test to fill many roles
- Test data is just data! You can combine it, transform it, generate it – whatever you need
- Model your side-effects with data! Mocks should be driven from data
- Test code is still code. Follow good DoP principles. Introduce types. Model the domain
- Parameterize your fixtures / helper functions to show which values are under test
- Hard coded data creates secret couplings and brittle test suites
- The damage of hard coding happens slowly over months and years
- Remove coupling by randomizing attributes that aren’t directly used in your test
- Randomization makes creating secret dependencies impossible
- Use Stream.generate to create infinite test data
- Visualize the set of states denoted by your data. Let it guide your testing
- Randomization lets us explore states rarely exercised in example-based testing
- Randomizing values is a light weight way of formally saying “for all (∀)” as part of a test
- Randomization is no silver bullet. It’s just another tool
- The work of generating random values forces asking good data-oriented questions
- Testing is more art than science. Embrace experimentation!
- Watch out for equating coverage with quality. The former is a poor measure of the latter
- Invest in more data coverage and tools for generating it
- There is no single “right” way of testing. Tailor it to your application
- Embrace literate programming in tests. Comments are a gift
- Good comments explain “why” not “what”
- The best tests are the ones you don’t have to write! Use good data-oriented modeling to make illegal states unrepresentable.
FAQ
What is the main idea of data-oriented testing in this chapter?
Data-oriented testing means structuring tests around inputs and outputs so the test itself clearly shows what is being exercised. By treating test data as first-class, tests become smaller, clearer, more flexible, and often more powerful.
Why are data-oriented codebases easier to test?
Because they are built around pure, deterministic functions that take data in and return data out. That reduces the need for mocks, side-effect control, and environmental setup, making tests simpler and more direct.
What is the difference between unit tests and integration tests in this chapter?
Unit tests run in isolation with dependencies mocked if needed. Integration tests involve dependencies and IO, ranging from small checks like talking to a database to full end-to-end scenarios.
How do data-oriented tests make assertions easier to understand?
They connect the setup data directly to the assertion, so the reader can see why an outcome is expected. Instead of relying on hidden setup or magic numbers, the test explains itself through the data it uses.
Why is shared test setup often a problem?
Shared setup hides important details outside the test body and creates coupling between tests and hidden constants. When that shared data changes, many tests can break even if their intent hasn’t changed.
How do factory functions help improve test data?
Factory functions let tests create data through an API that highlights only the relevant parts. This reduces noise, makes the important attributes obvious, and helps the test communicate its intent more clearly.
What is the purpose of randomizing unused inputs in tests?
Randomizing unused inputs prevents tests from accidentally depending on hard-coded hidden values. It increases confidence that the behavior holds for many possible states, not just one convenient example.
What is a power set and why is it useful in testing?
A power set is the set of all subsets of a collection. In tests, it helps exercise many combinations of inputs automatically, including edge cases like the empty set, so more scenarios are covered with less manual effort.
How can randomized data be used to test side effects?
Even when code does not return useful values, you can still describe expected behavior with data. The test can generate input states, trigger the side effect, and then verify the mock or external interaction using the same data model.
What is the chapter’s view on test coverage versus data coverage?
The chapter argues that data coverage matters more than line coverage. A test can achieve high code coverage and still prove very little, while well-modeled data-driven tests can provide much stronger confidence in correctness.
Data-Oriented Programming in Java ebook for free