We often see tests that are hard to understand because they drive code that calls objects returned from other objects. Here's an example of code that calls a chain of objects.
public void allUnresolvedIssues(IssueHandler handler) throws Failure {
TicketManager ticketManager = connections.getTicketManager();
ticketManager.lock();
try {
IssueManager issueManager = ticketManager.getIssueManager();
Iterator issues = issueManager.allIssuesIterator();
while (issues.hasNext()) {
Issue issue = issues.next();
if (issue.isUnresolved()) {
handler.accept(issue);
}
}
} finally {
ticketManager.unlock();
}
}
This is a minor offender, but even so it gets a list of
Issues from an
IssueManager, which it gets from a
TicketManager, which it gets from a
Connection. The important part of the method is the filtering of the issues, but to get there it has to navigate three objects, including locking and unlocking the TicketManager. We might have to do something like this to test it.
public void testFiltersUnresolvedIssuesFromIssueManager() throws Failure {
final ArrayList allIssues = new ArrayList() {{
add(issue1Unresolved);
add(issue2);
add(issue3Unresolved);
}};
expects(new InAnyOrder() {{
allowing(connections).getTicketManager(); will(returnValue(ticketManager));
expects(new InThisOrder() {{
one(ticketManager).lock();
one(ticketManager).getIssueManager(); will(returnValue(issueManager));
one(issueManager).allIssuesIterator(); will(returnValue(allIssues.iterator()));
one(issueHandler).accept(issue1Unresolved);
one(issueHandler).accept(issue3Unresolved);
one(ticketManager).unlock();
}});
}});
moreThanImmediateNeighbours.allUnresolvedIssues(issueHandler);
}
I find this confusing to read. First, too much of the test code is for setting up state to get to the interesting feature. Second, the test is exercising several things at once: locking and unlocking the TicketManager, querying the issues within the lock, and filtering the issues. If I want to test more conditions, such as when there are no issues or when something fails, I'll have to duplicate lots of irrelevant test code. I can extract that duplication into setup methods, but I think that's missing what the test is telling me.
Another problem is that the test is, more or less, just reproducing the logic of the code, which means it's not adding much value and also makes it brittle when I want to refactor later—especially if I have many similar tests to change. This is a common reason given for not using Mock Objects and a plausible one when dealing with code like this.
Let's see if I can break things up a bit. My first thought is that I can separate out everything to do with the TicketManager by extracting a method.
public void allUnresolvedIssues(IssueHandler handler) throws Failure {
TicketManager ticketManager = connections.getTicketManager();
ticketManager.lock();
try {
retrieveUnresolvedIssues(ticketManager.getIssueManager(), handler);
} finally {
ticketManager.unlock();
}
}
private void retrieveUnresolvedIssues(IssueManager issueManager, IssueHandler handler) {
Iterator issues = issueManager.allIssuesIterator();
while (issues.hasNext()) {
Issue issue = issues.next();
if (issue.isUnresolved()) {
handler.accept(issue);
}
}
}
Now I can see a bit more clearly the two activities going on here: getting hold of the issue manager, and retrieving issues from it. I can break these up into different, focussed objects. I could start from the current design and refactor, but I want to begin again to show the design process...