Static, extends and how they affect your unit tests

Jan Groothuijse

Jan Groothuijse

Senior Software Developer at Avisi

Published: 24 September, 2020

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.

blog-unittest

The problem with static methods

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.

Example using static method

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

Solution

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:

  • Create an interface with the static method's signature, convert the methods to non-static.
  • Move the static methods into an implementation of that interface.
  • Add a field in your caller with this interface, let dependency injection instantiate it.
  • Use this field, which is typed as the interface with the non-static methods, instead of directly calling the static methods

Example avoiding static methods

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

Isolation

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 with inherited methods

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.

Example using inherited method

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

Solution

So, how do we solve this? Simple, we change the relation from is-a to has-a.

  • Make the parent concrete, remove abstract methods if needed. Optionally copy the methods signatures to an interface, to be implemented by the subclass.
  • Remove the extends declaration and add a field for the former parent class. 
  • Call the methods on this field instead of calling parent methods.

This transformation is an example of choosing Composition over inheritance

Example avoiding inherited methods

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

Extension 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.

Other advantages

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:

  • Dependency injection instead of static and extends also enables spring's proxy-based aspect-oriented-programming. In turn, AOP allows us to specify things like authorization, caching, logging  etc.
  • Using mocks in tests, it becomes easier to test alternative flows, including exception handling.

Final words

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.

Related blogs

Did you enjoy reading?

Share this blog with your audience!