The Pros and Cons of Increasing the Size of the Tested Unit
After we have finished this lesson, we:
- Can identify the pros and cons of increasing the size of the tested unit.
- Understand when we should increase the size of the tested unit.
This lesson assumes that:
Get the source code from Github
The Pros of Increasing the Size of the Tested Unit
The pros of increasing the size of the tested unit are:
First, we can write meaningful tests for code which doesn’t have a lot of business logic.
For example, if we are writing tests for a REST API which provides CRUD operations for different entities, and we use the traditional approach (one test class per class), the tests of our web layer are quite shallow because they cannot test the logic that’s found the service layer.
On the other hand, if we increase the size of the tested unit, we can write fast tests which ensure that:
- When the requested information is found from the database, our REST API returns a Json document that contains the correct information.
- When valid information is provided to our REST API, our REST API saves the correct information to the database.
- When invalid information is provided to our REST API, our REST API returns a Json document that contains all validation errors.
- When invalid information is provided to our REST API, our REST API doesn’t make any changes to the database.
I think that these tests are more meaningful than the traditional class tests.
Second, we can refactor the implementation of the system under test as long as we don’t make any changes to its API or to the API of the components which are replaced with test doubles.
In other words, if we increase the size of the tested unit, we can write tests which test the behavior of the system under test instead of its implementation. This makes our application easier (and faster) to maintain because we can make some changes to its code without making any changes to our unit tests.
Third, we can cover more code with fast tests which are a bit like integration tests. This can be extremely useful if we are working with legacy code that’s hard to test.
Quite often the problem is that we cannot write the traditional class tests for a legacy system because it’s either hard or impossible to replace the dependencies of the tested class with test doubles. If we increase the size of the tested unit, we have to replace only the dependencies which interact with external APIs, file systems, or databases. This makes our life easier because typically the dependencies of the lowest layer are easier to replace than the dependencies found from the upper layers.
Next, we will take a look at the cons of this technique.
The Cons of Increasing the Size of the Tested Unit
The cons of increasing the size of the tested unit are:
First, the code that configures our test doubles can be hard to read because it might not be clear why the code is needed. The problem is that if our test doubles aren’t the dependencies of the class which provides the tested API, their configuration isn’t obvious if we aren’t familiar with the implementation of the tested unit.
If we run into this problem, we can make our test code a bit better by using these techniques:
- Configure test doubles by using private utility functions (or methods) and give proper names to these functions (or methods). A good name describes why our test case requires the configuration provided by our utility function (or method).
- Add comments to the functions (or methods) which configure our test doubles. Sometimes it’s hard to figure out a good name (or even a decent name) for a utility function (or method) that configures our test double. If this happens, we can make the situation a bit better by writing a comment which explains the purpose of our utility function (or method).
Second, the code that configures our test doubles can be hard to write if the system under test has a lot of business logic. If the system under test has a lot of business logic, writing tests which visit each one of the possible branches might mean that our test doubles have to return a lot of different objects when the system under test uses an external API, reads data from a file system, or gets information from the database. This means that we have to write a lot of stubbing code.
We can try to solve this problem by using these techniques:
- Write nested tests which help us to eliminate duplicate configuration code and allow us to structure our test class in a way that makes it less cumbersome to write.
- Replace stubs with fakes which return hard coded data. This technique allows us to remove the code which configures our stubs.
Let’s move on and find out when we should increase the size of the tested unit.
When Should We Increase the Size of the Tested Unit?
We should increase the size of the tested unit if any of the following conditions is true:
First, we are writing tests for finder functions (or methods). Because these functions (or methods) don’t typically have any business logic, it doesn’t really make sense to test these functions by writing class tests. If we increase the size of the tested unit, we can write meaningful and fast tests for our finder functions (or methods).
Second, we are writing tests for a CRUD API which doesn’t have a lot of business logic. If we increase the size of the tested unit, we can write fast tests which specify the behavior of our API. In other words, because our tests don’t depend on the implementation of our API, we can refactor the implementation of our API as long as we don’t make any changes to the public API or to the API of the components which are replaced with test doubles.
Third, we are writing tests for code whose dependencies don’t use external APIs, file systems, or databases. If we are in this situation, it doesn’t really make sense to write class tests unless the tested classes have a lot of business logic. If the tested classes don’t have a lot of business logic, we should increase the size of the tested unit because it allows us to specify the behavior of the system under test in a way that doesn’t limit our ability to refactor its implementation.
Fourth, we are working with legacy code that’s hard to test and we need fast tests. If we are working with legacy code, writing integration tests is the often the “easiest and cheapest” choice because often it’s hard to replace the dependencies of a single class with test doubles. However, integration tests are slower than unit tests. If we need fast tests, we should increase the size of the tested unit. This makes our life easier because typically the dependencies of the lowest layer are easier to replace than the dependencies found from the upper layers.
We can now identify the pros and cons of increasing the size of the tested unit, and we know when we should use this technique. Let’s summarize what we learned from this lesson.
Summary
This lesson has taught us three things:
- If we increase the size of the tested unit, we can write fast tests which specify the behavior of the system under test and allow us to refactor its implementation.
- The downside of increasing the size of the tested unit is that the code that configures our test doubles can be hard to read and write.
- We should increase the size of the tested unit if we want to write fast test which cover a lot of code and the system under test doesn’t have a lot of business logic.