The “Best Practices” of Nested Unit Tests
After we have finished this lesson, we
- Understand how we can eliminate duplicate code.
- Know how we can write nested unit tests that are easy to read, write, and maintain.
This lesson assumes that:
Watch the Lesson
The text version of this lesson is given in the following:
Introducing Five “Best Practices” of Nested Unit Tests
We know how we can write nested unit tests and we know that JUnit invokes the setup and teardown methods by following the class hierarchy of the invoked test method. However, we have no idea how we can leverage this information.
That is why this lesson presents five “best practices” that help us to get started.
1. Configure the System Under Test in the Setup Method of the Root Class
We should configure the system under test in the setup method that is found from the root class of our class hierarchy. To be more specific, this setup method should:
- Create all dependencies of the system under test.
- Create a new instance of the tested class.
For example, the source code of our test class could look as follows:
public class TaskServiceTest { private TaskRepository repository; private TaskService service; @Before public void configureSUT() { repository = mock(TaskRepository.class); service = new TaskService(repository); } }
Because a method that is annotated with the @Before
annotation is run before a test method is invoked, this technique ensures that the end state of a finished unit test cannot interfere the execution of the next unit test because every unit test is run against a fresh instance of the tested class.
After we have configured the system under test, we have to write our test methods. Let’s find out how we can create a class hierarchy that helps us to write clean unit tests which have no duplicate code.
2. Create One Inner Class per Tested Method
After we have configured the system under test, the obvious next question is:
Where should we put our test methods?
I think that we should follow these two rules:
- We should create one inner class per tested method.
- We should use the name of tested method as the name of the inner class.
If we create a test class that has tests for the findById()
and save()
methods of the TaskService
class, its source code looks as follows:
public class TaskServiceTest { private TaskRepository repository; private TaskService service; @Before public void configureSUT() { repository = mock(TaskRepository.class); service = new TaskService(repository); } public class FindById { } public class Save { } }
This technique has three benefits:
- We can put the required setup code to different contexts (aka inner classes). This makes our tests easier to read because basically we create a link between the setup code and our test cases.
- Because most IDEs allows us to open and close inner classes, we can select the inner class that interest us and close the other inner classes. Again, this makes our tests easier to read because the reader can concentrate on specific test cases and hide the other test cases.
- Even though it might not be obvious (yet), putting our test cases inside nested inner classes helps us to eliminate duplicate code that makes our tests hard to write and maintain.
So, should we put our test methods to these inner classes? I think that the answer to this question becomes obvious when we take a look at the next “best practice”.
3. Separate Different Test Cases by Using Inner Classes
It’s quite common that we have to write unit tests which require a bit different configuration and have different expectations for the expected outcome.
For example, if we are writing unit tests for a service method that returns a Task
object whose id
is given as a method parameter, it’s a good idea to ensure that our code is working as expected when:
- The
Task
object is not found. - The
Task
object is found.
Before we will write these unit tests, it’s a good idea to separate the actual test methods by using inner classes and name these inner classes by prepending the word ‘When’ to the description of the tested scenario.
In other words, if we are writing unit tests for the findById()
method of the TaskService
class, we have to add two new inner classes to the FindById
class:
- The
WhenTaskIsNotFound
class contains the test code which ensures that thefindById()
method is working as expected when the requested task is not found. - The
WhenTaskIsFound
class contains the test code which ensures that thefindById()
method is working as expected when the requested task is found.
After we have added these inner classes into the FindById
inner class, the source code of our test class looks as follows:
public class TaskServiceTest { private TaskRepository repository; private TaskService service; @Before public void configureSUT() { repository = stub(TaskRepository.class); service = new TaskService(repository); } public class FindById { public class WhenTaskIsNotFound { } public class WhenTaskIsFound { } } }
Also, if the findById()
method has code that is invoked in both cases and we want to test that code, we should put these these test methods to the FindById
class.
This technique enhances these three benefits:
- We create a stronger link between our setup code and our test cases.
- Our code is easier to read because the reader can navigate our class hierarchy and select the test cases that interest him or her.
- We can eliminate duplicate code by putting our setup code to the right place.
I have mentioned a few times that we can eliminate duplicate code by putting our setup code to the right place. Next, we will find out how we can identify the right place for our setup code.
4. Create Test Data and Configure Test Doubles as High as Possible
If we want to eliminate duplicate code, we have to reuse the code that creates our test data and configures our test doubles. The best way to do this is to put this code to a setup method that is invoked before the test methods that require this setup code.
Let’s take a look at two examples that help us to understand this principle.
First, if we are writing unit tests for the findById()
method of the TaskService
class, we have to create our test data and configure our test doubles in the lowest classes of our test class hierarchy because our test cases do not use the same configuration.
After we have created our test data and configured our test doubles, the source code of our test class looks as follows:
public class TaskServiceTest { private TaskRepository repository; private TaskService service; @Before public void configureSUT() { repository = stub(TaskRepository.class); service = new TaskService(repository); } public class FindById { public class WhenTaskIsNotFound { @Before public void returnNoTask() { given(repository.findById(1L)).thenReturn(Optional.empty()); } } public class WhenTaskIsFound { private Task found; @Before public void returnFoundTask() { found = new TaskBuilder() .withTitle("Foo") .build(); given(repository.findById(1L)).thenReturn(Optional.of(found)); } } } }
Second, if we are writing unit tests for a method that always fetches a Task
object from the database, we should create a new Task
object and configure our test double as high in the class hierarchy as possible.
After we have created a new Task
object and configured the TaskRepository
mock to return it when its findById()
method is invoked, the source code of our test class looks as follows:
public class TaskServiceTest { private TaskRepository repository; private TaskService service; @Before public void configureSUT() { repository = stub(TaskRepository.class); service = new TaskService(repository); } public class MethodX { private Task found; @Before public void returnTask() { found = new TaskBuilder() .withTitle("Foo") .build(); given(repository.findById(1L)).thenReturn(Optional.of(found)); } @Test public void testX() { } public WhenA { @Before public void setupA() { } @Test public void testA() { } } public class WhenB { @Before public void setupB() { } @Test public void testB() { } } } }
This technique has two benefits:
- We have to create the required
Task
object and configure theTaskRepository
stub only once. The tests methods that are found from theMethodX
,WhenA
, andWhenB
classes use the same configuration. This means that if we make changes to the API of thefindById()
method, we have to make these changes only to one place. - Our tests are easier to read because our setup code is divided into multiple setup methods, and our test methods are quite small and contain only assertions or code that verifies interactions that happened between the system under test and a mock.
We have gone a long way but we haven’t talked about one very important aspect of unit testing. Next, we will learn to put our constants to the right place.
5. Declare Constants as Low as Possible
If we are writing traditional unit tests, the odds are that we are putting our constants to the beginning of our test class. The problem of this approach is that typically our test class has a huge list of constants and we have no way to link these constants with the test cases that use them. This makes our tests hard to read.
We can solve this problem and link our constants with the test cases that use them by declaring our constants as low in the class hierarchy as possible.
For example, if we are writing unit tests for the findById()
method of the TaskService
class, we can declare the required constants by following these steps:
- Add a
TASK_ID
constant into theFindById
class. We have to declare this constant in this class because it is used by the tests found from theWhenTaskIsNotFound
andWhenTaskIsFound
classes. - Add a
TITLE
constant into theWhenTaskIsFound
class. We have to declare this constant in this class because it is used by the test methods found from theWhenTaskIsFound
class.
The source code of our test class looks as follows:
public class TaskServiceTest { private TaskRepository repository; private TaskService service; @Before public void configureSUT() { repository = stub(TaskRepository.class); service = new TaskService(repository); } public class FindById { private final Long TASK_ID = 1L; public class WhenTaskIsNotFound { @Before public void returnNoTask() { given(repository.findById(TASK_ID)) .thenReturn(Optional.empty()); } } public class WhenTaskIsFound { private final String TITLE = "Foo"; private Task found; @Before public void returnFoundTask() { found = new TaskBuilder() .withTitle(TITLE) .build(); given(repository.findById(TASK_ID)) .thenReturn(Optional.of(found)); } } } }
As we can see, our test class is easy to read because we can see immediately that:
- The
TASK_ID
constant is used by the tests found from theWhenTaskIsNotFound
andWhenTaskIsFound
classes. - The
TITLE
constant is used by the tests found from theWhenTaskIsFound
class.
I admit that this is a minor improvement, but when we use this technique together with the other techniques described in this lesson, we can make a big difference.
Let’s summarize what we learned from this lesson.
Summary
This lesson has taught us three things:
- We can make our tests easier to read by creating a class hierarchy that separates different test cases.
- We can remove duplicate code by creating our test data and configuring our test doubles as high in the class hierarchy as possible.
- We can link our constants with the test cases that use them by declaring our constants as low in the class hierarchy as possible.
Previous LessonNext Lesson