Test With Spring Course
Save Time by Writing Less Test Code
  • Home
  • Pre-Sales FAQ
  • Support
  • Log In

/ August 14, 2016 / petrikainulainen

The “Best Practices” of Nested Unit Tests

Lesson Progress:
← Back to Topic

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:

  • You know how you can write nested unit tests with JUnit 4

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 the findById() method is working as expected when the requested task is not found.
  • The WhenTaskIsFound class contains the test code which ensures that the findById() 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 the TaskRepository stub only once. The tests methods that are found from the MethodX, WhenA, and WhenB classes use the same configuration. This means that if we make changes to the API of the findById() 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:

  1. Add a TASK_ID constant into the FindById class. We have to declare this constant in this class because it is used by the tests found from the WhenTaskIsNotFound and WhenTaskIsFound classes.
  2. Add a TITLE constant into the WhenTaskIsFound class. We have to declare this constant in this class because it is used by the test methods found from the WhenTaskIsFound 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 the WhenTaskIsNotFound and WhenTaskIsFound classes.
  • The TITLE constant is used by the tests found from the WhenTaskIsFound 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 Lesson Next Lesson →

Can I help you?

This is a free sample lesson of my Test With Spring course. If this lesson helped you to solve your problem, you should find out how my testing course can help you.

Support and Privacy

  • Pre-Sales FAQ
  • Support
  • Cookie Policy
  • No Bullshit Privacy Policy
  • No Bullshit Terms and Conditions

Test With Spring Course

  • Starter Package
  • Intermediate Package
  • Master Package

Free Sample Lessons

  • Introduction to JUnit 4
  • Introduction to Unit Testing
  • Introduction to Integration Testing
  • Introduction to End-to-End Testing
  • Introduction to Spock Framework
  • Introduction to Integration Testing – Spock Edition
  • Writing End-to-End Tests With Spock Framework

Copyright Koodikupla Oy 2016 — Built on Thesis by Themedy