- Roles of Automated Tests
- Refactorings Affecting Tests
- Another Refactoring: Extract Superclass
- What Tests Should We Add?
- When Should We Add Tests?
- Bottom Line
Another Refactoring: Extract Superclass
Suppose we Extract Superclass on a class. To do this, we'll create a new parent class and move data and methods to it. By default, this need not affect test clients; they can still manipulate an instance of the original class, now a subclass, as shown in Figure 6.
Figure 6 Extract Superclass.
But presumably the reason we extracted the new class is that we have other uses for it. We'd like to create and use other subclasses. But can these subclasses trust their new parent? It hasn't been tested on its own, but rather only in the context of the original class. So we need additional tests focused on the new superclass, as shown in Figure 7.
Figure 7 Extract Superclass showing test clients.
When we create a new subclass, it will have its own test as well. But it will have some assurance that the superclass does its job properly.
Example
Many algorithms for searching in graphs or other structures have a common form, something like this:
Stack candidates; while (!stack.isEmpty()) { Something x = (Something) candidates.pop(); if (x.acceptable()) return x; pushMoreCandidates(stack, x); }
This version of the algorithm uses a stack to manage the candidates, although many variations don't rely on the stack discipline: They just want a new candidate to work with. (Other disciplines include queue, priority, random, and so on)
Suppose we extract a Candidates class to encapsulate that decision, as shown in Figure 8.
Figure 8 Extracting a Candidates class.
How Is It Tested?
The algorithm uses the stack to hold a set of candidate values. Suppose the original algorithm is constructed in such a way that it never generates duplicate candidates. Then no test of the algorithm will be able to ascertain whether the stack is a true stack, or one that ignores duplicates.
However, once Candidates is extracted, it may be used in new contexts. If the next use requires set behavior, it might failor fail to terminatewithout it.
A test that's good enough before refactoring might not be good enough afterward.
Consider the tests:
Some are testing the algorithm with little regard to the stack.
Others test the stack with little regard to the algorithm.
Finally, some tests focus on the interaction between the algorithm and the stack.
To make our tests best accommodate the new structure, we want to move tests around. Tests of the algorithm can stay where they are. Tests of the stack can move to a new test class focusing on testing the Candidates. Extracting this class will expose some previously hidden behaviors; we need to fill out the Candidates tests by adding tests for those behaviors, as shown in Figure 9.
Figure 9 Creating a new test client.
Tests of the interaction are the most interesting. Earlier, these tests were trying to test the Algorithm and the Stack together. To do this, those tests tried to force the Stack into different states, but it may not have been possible to use the Algorithm to get the Stack into every state we would like to test. With the Candidates class now standing outside the Algorithm, it should be possible to better test that part of the code. This may let us simplify the tests in the Algorithm that were focused on the interaction; those tests can focus on the parts that are interesting from the point of the view of the Algorithm and not try to test Candidates as well.
Refactoring may cause you to add, move, or delete tests.
Sometimes someone says, "I need to expose the private parts of this class so I can be sure what's going on with it." This may indicate that there's a hidden class, just as Candidates was hidden in Algorithm. Extracting the class lets us test it independently, and reduces the urge to expose parts of the original class unnecessarily. This also helps the tests be robust, as they're not using internal detailsthey respect the class's secret.