Book review: Fifty quick ideas to improve your tests - Part 2
This article is part 2 of my review of the book "Fifty quick ideas to improve your tests". I'll continue to share some of my personal highlights with you.
Replace multiple steps with a higher-level step
If a test executes multiple tasks in sequence that form a higher-level action, often the language and the concepts used in the test explain the mechanics of test execution rather than the purpose of the test, and in this case the entire block can often be replaced with a single higher-level concept.
When writing a unit test for some complicated object, you may be collecting some data first, then putting that data into value objects, maybe wrapping them in other value objects, before you can finally pass them to the constructor of the value object. At that point you may have to call several methods on the object before it's in the right state. The same goes for acceptance tests. You may need quite a number of steps before you get to the point where you can finally write a 'When' clause.
I find that often the steps leading up to the 'When' clause (or to the 'Act' part of a unit test), can be summarized as one thing. This is meant by the "single higher-level concept". So instead of enumerating everything that has happened to the system, you look for a single step that described what your starting point is. For instance, instead of:
Given the user logged in And they have created a purchase order And they added product A to it, with a quantity of 10 And they placed the purchase order When the user cancels the purchase order Then ...
You could summarize the 'Given' steps as follows:
Given a placed purchase order for product A, with a quantity of 10 When the user cancels the purchase order Then ...
Once you start looking for ways to make things smaller and simpler, easier to follow and read, you'll find that many of the details you previously added to the smaller steps are completely irrelevant for the higher-level step. In our case, the product and the quantity may be completely irrelevant for the scenario that deals with cancelling the purchase order. So the final version may well be:
Given a placed purchase order When the user cancels the purchase order Then ...
When implementing the step definition for this single, simplified step, you still need to provide sensible default data. But it happens behind the scenes. This technique hides the details that are irrelevant, and only shows the relevant ones to the person reading the scenario. It will be a lot easier to understand what's being tested with this particular scenario.
I find that this approach works really well with unit tests too. One test case may show how a complicated object can be constructed, another test case won't just repeat all those steps, but will summarize it properly (like we did for the above scenario). This often requires the introduction of a private method in the test class, which wraps the individual steps. The method is the abstraction, its name the higher-level concept.
Specification, acceptance tests, living documentation
When creating your artifacts, remember the three roles they must serve at different times: now as a specification, soon as acceptance tests, and later as living documentation. Critique the artifact from each perspective. How well does it serve each distinct role? Is it over-optimised for one role to the detriment of others?
In workshops I often point out that testing is a crucial part of the development workflow. You need it to verify that the unit of code you've been working works as expected. To improve this perspective on writing tests, you could aim for not just verifying correctness of a unit, but specifying it. This implies some kind of test-first approach, where you specify expected behavior. The next level would be to consider the future. What happens when you're done writing this code? Could someone else still understand what's going on? Do you only have technical specifications, or also domain-level specifications?
For unit tests, the danger is that you'll be doing only technical verifications. You may be testing methods, arguments and return values instead of higher-level behaviors. It happens when you're just repeating the logic of the class inside its unit test. You find yourself looking into the box (white box testing), instead of treating the box as an object with proper boundaries.
The best thing you can do is to think hard about
Truncated by Planet PHP, read more at the original (another 5202 bytes)