Three Reasons Why We Should Not Use Inheritance In Our Tests
When we write automated tests (either unit or integration tests) for our application, we should notice pretty soon that
- Many test cases use the same configuration which creates duplicate code.
- Building objects used in our tests creates duplicates code.
- Writing assertions creates duplicate code.
The first thing that comes to mind is to eliminate the duplicate code. As we know, the Don’t repeat yourself (DRY) principle states that:
Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.
So we get to work and remove the duplicate code by creating a base class (or classes) which configures our tests and provides useful testing utility methods to its subclasses.
Unfortunately, this is a very naive solution. Keep on reading and I will present three reasons why we should not use inheritance in our tests.
- Inheritance Is Not the Right Tool for Reusing Code
- If we want to ensure that only the relevant code is visible to our test classes, we probably have to create a “complex” class hierarchy because putting everything in one superclass isn’t very “clean”. This makes our tests very hard to read.
- Our test classes are in the mercy of their superclass(es), and any change which we make to a such superclass can effect its every subclass. This makes our tests “hard” to write and maintain.
- Inheritance Can Have a Negative Effect to the Performance of Our Test Suite
- Before JUnit invokes the tests of a test class, it looks for methods which are annotated with the @BeforeClass annotation. It traverses the whole class hierarchy by using reflection. After it has reached to java.lang.Object, it invokes all methods annotated with the @BeforeClass annotation (parents first).
- Before JUnit invokes a method which annotated with the @Test annotation, it does the same thing for methods which are annotated with the @Before annotation.
- After JUnit has executed the test, it looks for method which are annotated with the @After annotation, and invokes all found methods.
- After all tests of a test class are executed, JUnit traverses the class hierarchy again and looks for methods annotated with the @AfterClass annotation (and invokes those methods).
- The traversal of the test class hierarchy is wasted CPU time.
- Invoking the setup and teardown methods is wasted CPU time if our tests don’t need them.
- Using Inheritance Makes Tests Harder to Read
- Tests document the way our code is working right now.
- Tests ensure that our code is working correctly.
- If our tests are easy to read, it is easy to understand how our code works.
- If our tests are easy to read, it is easy to find the problem if a test fails. If we cannot figure out what is wrong without using debugger, our test isn’t clear enough.
DZone published a very good interview of Misko Hevery where he explains why inheritance is not the right tool for reusing code:
The point of inheritance is to take advantage of polymorphic behavior NOT to reuse code, and people miss that, they see inheritance as a cheap way to add behavior to a class. When I design code I like to think about options. When I inherit, I reduce my options. I am now sub-class of that class and cannot be a sub-class of something else. I have permanently fixed my construction to that of the superclass, and I am at a mercy of the super-class changing APIs. My freedom to change is fixed at compile time.
Although Misko Hevery was talking about writing testable code, I think that this rule applies to tests as well. But before I explain why I think this way, let’s take a closer look at the the definition of polymorphism:
Polymorphism is the provision of a single interface to entities of different types.
This is not why we use inheritance in our tests. We use inheritance because it is an easy way to reuse code or configuration. If we use inheritance in our tests, it means that
So, why does this matter? It matters because tests are code too! That is why this rule applies to test code as well.
By the way, did you know that the decision to use inheritance in our tests has practical consequences as well?
If we use inheritance in our tests, it can have a negative effect to the performance of our test suite. In order to understand the reason for this, we must understand how JUnit deals with class hierarchies:
In other words, we are wasting CPU time in two ways:
I learned this from a book titled Effective Unit Testing.
You might of course argue that this isn’t a big problem because it takes only a few milliseconds per test case. However, the odds are that you haven’t measured how long it really takes.
Or have you?
For example, if this takes only 2 milliseconds per test case, and our test suite has 3000 tests, our test suite is 6 seconds slower than it could be. That might not sound like a long time but it feels like eternity when we run our tests in our own computer.
It is in our best interest to keep our feedback loop as fast as possible, and wasting CPU time doesn’t help us to achieve that goal.
Also, the wasted CPU time is not the only thing that slows down our feedback loop. If we use inheritance in our test classes, we must pay a mental price as well.
The biggest benefits of automated tests are:
We want to make our tests easy to read because
That is nice but it doesn’t really explain why using inheritance makes our tests harder to read. I will demonstrate what I meant by using a simple example.
Let’s assume that we have to write unit tests for the create() method of the TodoCrudServiceImpl class. The relevant part of the TodoCrudServiceImpl class looks as follows:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class TodoCrudServiceImpl implements TodoCrudService { private TodoRepository repository; @Autowired public TodoCrudService(TodoRepository repository) { this.repository = repository; } @Transactional @Overrides public Todo create(TodoDTO todo) { Todo added = Todo.getBuilder(todo.getTitle()) .description(todo.getDescription()) .build(); return repository.save(added); } //Other methods are omitted. }
When we start writing this test, we remember the DRY principle, and we decide to create a two abstract classes which ensure that we will not violate this principle. After all, we have to write other tests after we have finished this one, and it makes sense to reuse as much code as possible.
First, we create the AbstractMockitoTest class. This class ensures that all test methods found from its subclasses are invoked by the MockitoJUnitRunner. Its source code looks as follows:
import org.junit.runner.RunWith; import org.mockito.runners.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) public abstract class AbstractMockitoTest { }
Second, we create the AbstractTodoTest class. This class provides useful utility methods and constants for other test classes which tests methods related to todo entries. Its source code looks as follows:
import static org.junit.Assert.assertEquals; public abstract class AbstractTodoTest extends AbstractMockitoTest { protected static final Long ID = 1L; protected static final String DESCRIPTION = "description"; protected static final String TITLE = "title"; protected TodoDTO createDTO(String title, String description) { retun createDTO(null, title, description); } protected TodoDTO createDTO(Long id, String title, String description) { TodoDTO dto = new DTO(); dto.setId(id); dto.setTitle(title); dto.setDescrption(description); return dto; } protected void assertTodo(Todo actual, Long expectedId, String expectedTitle, String expectedDescription) { assertEquals(expectedId, actual.getId()); assertEquals(expectedTitle, actual.getTitle()); assertEquals(expectedDescription, actual.getDescription()); } }
Now we can write a unit test for the create() method of the TodoCrudServiceImpl class. The source code of our test class looks as follows:
import org.junit.Before; import org.junit.Test; import org.mockito.Mock; import static org.mockito.Matchers.isA; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; public TodoCrudServiceImplTest extends AbstractTodoTest { @Mock private TodoRepository repositoryMock; private TodoCrudServiceImpl service; @Before public void setUp() { service = new TodoCrudServiceImpl(repository); } @Test public void create_ShouldCreateNewTodoEntryAndReturnCreatedEntry() { TodoDTO dto = createDTO(TITLE, DESCRIPTION); when(repositoryMock.save(isA(Todo.class))).thenAnswer(new Answer<Todo>() { @Override public Todo answer(InvocationOnMock invocationOnMock) throws Throwable { Todo todo = (Todo) invocationOnMock.getArguments()[0]; todo.setId(ID); return site; } }); Todo created = service.create(dto); verify(repositoryMock, times(1)).save(isA(Todo.class)); verifyNoMoreInteractions(repositoryMock); assertTodo(created, ID, TITLE, DESCRIPTION); } }
Is our unit test REALLY easy read? The weirdest thing is that if we take only a quick look at it, it looks pretty clean. However, when we take a closer look at it, we start asking the following questions:
- It seems that the TodoRepository is a mock object. This test must use the MockitoJUnitRunner. Where the test runner is configured?
- The unit test creates new TodoDTO objects by calling the createDTO() method. Where can we find this method?
- The unit test found from this class uses constants. Where these constants are declared?
- The unit test asserts the information of the returned Todo object by calling the assertTodo() method. Where can we find this method?
These might seem like “small” problems. Nevertheless, finding out the answers to these questions takes time because we have to read the source code of the AbstractTodoTest and AbstractMockitoTest classes.
If we cannot understand a simple unit like this one by reading its source code, it is pretty clear that trying to understand more complex test cases is going to be very painful.
A bigger problem is that code like this makes our feedback loop a lot longer than necessary.
What Should We Do?
We just learned three reasons why we should not use inheritance in our tests. The obvious question is:
If we shouldn’t use inheritance for reusing code and configuration, what should we do?
That is a very good question, and I will answer to it in a different blog post.
Reference: | Three Reasons Why We Should Not Use Inheritance In Our Tests from our JCG partner Petri Kainulainen at the Petri Kainulainen blog. |