This series of posts describes the process I use to decide which unit tests to write, and how to write them – regardless if the code already exists or not. This approach works for both TDD and test-after unit tests, but it works well where most of the systems are – legacy mode. The process is about understanding the environment and fitting in the unit tests, and also integration tests.
The process includes these steps:
- Understanding the problem
- Designing a solution
- Test categorization
- Testability design consideration
- Design constraints
- Analyze existing code
- Identify dependencies
- Define which code goes where
- Actually writing tests
- Re-check Unit tests, integration tests and accpetance tests assumptions and coverage.In the series of posts, I’ll go into each step in details.
Step 1: Understanding the problem.
The problem we’re trying to solve is a set of use cases, or user stories, or any name for something that gives someone or something value. Even if this is a story that was sliced down pretty thin, there can be several test cases (and therefore tests) for it. Although it’s possible, these are not necessarily unit tests – they can be system level tests or integration tests. They include all kinds of happy paths, validations, error handling, and others that the complete solution has to support. I usually use a mind map to draw these, and if possible have someone review it. Better yet, collaborate on building the map.
Step 2: Designing a solution.
This is a step that is needed if we haven’t written the code yet. But even if the feature is already done, or the bug is already fixed, it is still valuable to draw the existing design.
In TDD we’re told that the design will emerge from the unit tests. This is true, but your mileage may vary: Several designs can solve the problem, and you may not choose the best one. Thinking upfront can help avoid unnecessary waste.
So, even with TDD, define the main blocks of the system you’re going to write. The design helps identify the system boundaries, and where interaction with other system occur. This is a high-level design, and it may not be a final one. It should be enough to see if our design covers the test cases we’ve identified in step 1. It should be detailed enough to start writing the first unit tests.
If you’re adding unit tests to an existing design, you’ll need that design to see if the unit tests for the cases you want to add are feasible, and where to unit test what. You’ll also identify which things should be mocked, or if you need to refactor in order to introduce seams.
We’ll continue the series next time.
Also, check out the video of a my talk “Unit Testing Strategy“.