The design of your application also affects your unit tests. This blog post discusses how both the usage of static methods and inherited methods increases the scope of your unit tests and why that is a bad thing.
When calling a static method, the actual code to be called is already known at compile time. This is because, for a static method, the actual code to be called does not depend on the runtime type of the object the method was called upon like it would for a normal method. Unfortunately, that also means there is no way to mock or stub that call in a unit test.
A unit test targets the smallest possible unit of code. Its scope is as small as possible so that only changes in the system under test will result in changes to the test. In other words, a minor change in the application should lead to changes in only one unit test. Mocking and stubbing are used to achieve this goal. Instead of testing the functionality of other objects, those objects are replaced by test-doubles (mocks) that allow us to verify the system under test interacts with other objects the way we expect.
So, in the test of the caller of a static method, we cannot use mocks to verify the system under test's interaction. This means our test will include the static method's definition, increasing the scope of the test.
The problem described here can be best illustrated using an example. The running example application is a spring-boot REST API providing a random people, using randomuser.me as back-end. In the test code in Listing 2, we would only like to test that the fromRest method is called in Listing 1). However, we cannot do so directly. Instead, we must provide the actual input for the fromRest method, and then check the result against a reference value.
.getResults()
.stream()
.map(PersonMapper::fromRest)
.collect(Collectors.toList());
Listing 1) Snippet from PersonRepository using a static method
Note in Listing 1 PersonMapper::fromRest, which is reference to the static method fromRest in the class PersonMapper.
var list = Collections.singletonList(new Person(Gender.MALE, new Name("title", "first", "last")));
Mockito.when(resultContainer.getResults()).thenReturn(list);
var reference = Collections.singletonList(new nl.avisi.demo.model.Person(true, "first last"));
assertEquals(reference, sut.getAll());
Listing 2) Snippet from PersonRepositoryTest testing the static method
Do not use a static method. Instead, let concrete classes expose their dependencies using interfaces. This is the D in SOLID, Dependency inversion principle. So:
In Listing 3 mapper is now a field in the repository class. Note that mapper::fromRest refers to the non-static method fromRest in the class PersonMapper.
private final PersonMapper mapper;
...
.getResults()
.stream()
.map(mapper::fromRest)
.collect(Collectors.toList());
Listing 3) Snippet from PersonRepository avoiding static methods
Since the personMapper is a field, we are able to mock it in Listing 4. We can then proceed to instruct our mock to return a specific restPerson, when called with a certain person. Effectively, we are only testing that fromRest is called on the mapper, not the implementation of fromRest.
Mockito.when(resultContainer.getResults()).thenReturn(Collections.singletonList(restPerson));
Mockito.when(personMapper.fromRest(restPerson)).thenReturn(person);
assertEquals(Collections.singletonList(person), sut.getAll());
Listing 4) Snippet from PersonRepositoryTest testing the static method
Now that we got rid of our static method calls, our unit tests are isolated against changes outside the class we're testing. This is how we want our unit tests: only the implementation details of the class we are testing should affect our tests.
The problem discussed above also occurs when calling a non-static method defined by a parent class. Even though the method is non-static, it is implicitly called on this, a reference to the object itself, which can't be mocked. Just like the static method, its definition is outside the system under test. So calling an inherited method increases the scope of our unit test too. Since this problem involves the object hierarchy of the caller, the solution is slightly different.
As shown in Listing 6, in order to test PersonRepository, we must mock the interaction with the restTemplate. But the restTemplate is not part of PersonRepository, it is part of its parent: RandomUserBaseRepository. Therefore, this interaction should be outside the scope of our test. However, the get() cannot be redirected, since it is effectively called on this (this.get()). This is why calling base class methods will complicate your unit tests.
public class PersonRepository extends RandomUserBaseRepository {
...
return get("/api", type).getResults()
Listing 5) Snippet from PersonRepository using an inherited method
Note that the get() method in Listing 5 is defined by RandomUserBaseRepository.
var sut = new PersonRepository(restTemplate, baseConfig);
Mockito.when(restTemplate.exchange(Mockito.<requestentity>any(), Mockito.<parameterizedtypereference>any())).thenReturn(response);
</parameterizedtypereference</requestentity
Listing 6) Snippet from PersonRepositoryTest using an inherited method
So, how do we solve this? Simple, we change the relation from is-a to has-a.
This transformation is an example of choosing Composition over inheritance.
In Listing 7, instead of extending from RandomUserBaseRepository, PersonRepository has a field repositoryUtil providing the same method. This field is mocked in PersonRepositoryTest. Listing 8 shows how we instruct the mock to behave. Now that the behavior of get() is mocked, its implementation is no longer in scope for our test.
public class PersonRepository {
...
private final RandomUserRepositoryUtil repositoryUtil;
...
return repositoryUtil.get("/api", type).getResults()
Listing 7) Snippet from PersonRepository avoiding inherited methods
Mockito.when(repositoryUtil.get(Mockito.anyString(), Mockito.any())) .thenReturn(resultContainer);
Listing 8) Snippet from PersonRepositoryTest avoiding inherited methods
“But Kotlins extension methods are cool, right?” Perhaps not, extension methods are syntactic-sugar around a static method. This means calls cannot be redirected. Thus, we have a high coupling between the caller and the extension method. Extension methods do have one redeeming quality, however: they can be placed anywhere. This enables you to place them in the unit you are testing, thereby having them in scope for a unit test after all.
This article is not just about changing the code itself, to make our tests nicer. It's about putting time-honored object-oriented principles, such as low coupling and depending on abstractions, into practice. Adherence to these principles just happens to pay off in unit tests, but other advantages include:
Using static methods, especially extension methods, can be very useful. But now, you will know why your unit tests feel awkward, and why a simple change in your code will change so many tests. And more importantly, how to fix it.