Make services unit testable using dependency injection

I’m sure you have used an app or website that looks very good at the beginning...until you see a few bugs and glitches and then you lose faith and interest in it. 😔 You know why such things happen, and it all comes down to testing. The more automated tests you have, the less likely you are to find yourself in situations like this as a software developer.

There are many types and levels of testing, and one of the most important ones is unit testing. In unit testing, you want to make sure a method in a class works as expected. As you'll remember, unit testing is calling a method and checking its output against expectations. Here is an important principle in unit testing:Ā you always want to test one class at a time.

This means that at the time of testing, you need to isolate your class from other classes it might call (a.k.a. its dependencies). But how would you do that? If you're familiar with testing, you know the answer is to mock. You'll remember that mocks are objects that mimic the behavior of real objects. You replace dependencies of an object with their mock versions to stay in control when unit testing.

According to the three-tier architecture, service classes contain the bulk of business logic of a web application, so they have the highest priority for being unit tested...but they usually call repositories or other services. 😬

Ā I already know what testing is! Why are you telling me all of this? Just go ahead and write unit tests for our classes and mock the rest! šŸ™„

Well, I wish I could! But we have a problem in our code, and it’s called tight coupling.Ā 

Tight couplingĀ vs loose coupling

Let’s have a look at Ā getWatchlistItems()Ā  method from Ā WatchlistServiceĀ  class again:

	public List<WatchlistItem> getWatchlistItems() {
		List<WatchlistItem> watchlistItems = watchlistRepository.getList();
				
		for (WatchlistItem watchlistItem : watchlistItems) {
			Optional<String> movieRating = movieRatingService.getMovieRating(watchlistItem.getTitle());
			if (movieRating.isPresent()) {
				watchlistItem.setRating(movieRating.get());
			}
		}
		return watchlistItems;
	}

Let’s say we want to unit test this method which is calling methods from two other classes: Ā watchlistRepository.getList()Ā  and Ā movieRatingService.getMovieRating(...). So we need to mock Ā WatchlistRepositoryĀ  and Ā MovieRatingServiceĀ  to be able toĀ test Ā getWatchlistItems()Ā  method in isolation. In other words, we want an instance of Ā WatchlistServiceĀ in which it makes calls to mock version of its dependencies, rather than the real ones. Here is the problem, with the current design: mocking is not possible because of these two lines:

private WatchlistRepository watchlistRepository = new WatchlistRepository();
private MovieRatingService movieRatingService = new MovieRatingService();

Ā Do you see the problem? InĀ WatchlistService, we are creating instances of its dependencies right at top, and then we use them in methods like Ā getWatchlistItems().Ā Ā WatchlistServiceĀ  is tied to and calls the real instances of these services. There’s no way to replace those dependencies with their mocks at the time of unit testing. This is called tight coupling, and it is considered a bad practice in software design because changes of one component propagate to other components, which means more headaches!Ā šŸ¤•

You want to aim for loose coupling, where a class is not tied to any particular implementation of its dependencies, so those dependencies are easily replaceable. At the time of unit testing, you can replace them with mocks or at runtime you can replace them with some different implementations based on some conditions like the environment (we will see this later in Chapter 3). But how could you change services to become loosely coupled? Well, nothing to worry about because here comes dependency injection to the rescue! šŸš€

Let’s refactor for dependency injection!

The idea behind dependency injection is as follows:Ā Don’t create instances of your dependencies, declare your dependencies, and let someone else create instances of them and pass them to you.

It pretty much means saying goodbye to the JavaĀ Ā newĀ keyword when it comes to creating instances of other dependencies. For example, declare Ā WatchlistRepositoryĀ  and Ā MovieRatingServiceĀ  as constructor parameters of Ā WatchlistService, instead of creating new instances of them manually.Ā  Then at runtime, the Spring frameworkĀ will pass in real instances of them, and at the time of unit testing, pass in mocked versions of them.Ā Ā 

But how is a framework going to call the constructor of a class and pass in instances of other classes ...er...dependencies?

It happens by delegating the creation of all major components like controllers, services, and repositories to the Spring dependency injection framework. Simply ask the Spring framework to create and hold instances of these classes. Classes that are managed by Spring dependency injection framework are called Spring beans, and they are marked with special annotations. You are already familiar with one of them, which is the Ā @ControllerĀ  annotation. Let's use another annotation that turns service classes into Spring beans called aĀ @Service.

To tell Spring DI that you want some dependencies to be injected into a class constructor, use another annotation called Ā @Autowired.

Okay, enough talking. Let’s see them in practice!

First of all, you need to stop creating new instances of dependencies in Ā WatchlistĀ  service and instead define them as constructor parameters.Ā  Remove creating instances of Ā MovieRatingServiceĀ  and Ā WathclistRepositoryĀ  from theĀ Ā WatchlistĀ  service, and instead add a new constructor with an Ā @AutowiredĀ  annotation like this:

    private WatchlistRepository watchlistRepository;
	private MovieRatingService movieRatingService;

	@Autowired
	public WatchlistService(WatchlistRepository watchlistRepository, MovieRatingService movieRatingService) {
		super();
		this.watchlistRepository = watchlistRepository;
		this.movieRatingService = movieRatingService;
	}

The exact same process should be done for Ā WatchlistController. So let’s remove instantiation of Ā WatchlistService, and instead add a constructor with anĀ @AutowiredĀ annotation.

    private WatchlistService watchlistService;
	
	@Autowired
	public WatchlistController(WatchlistService watchlistService) {
		super();
		this.watchlistService = watchlistService;
	}

Now add Ā @ServiceĀ  annotation on top of Ā WatchlistService, Ā MovieRatingService, and Ā WatchlistRepository.Ā  And that’s it for this change. If you restart the server and give it a run, you’ll see the functionality stays unchanged.

One of the side benefits of using a DI framework is reduced boilerplate code. Creating new instances of dependencies in services and controllers is a form of boilerplate code, and by removing them, we actually have a cleaner codebase.

Add unit tests

Now it’s time to reap the main benefit of our refactoring effort, which is writing unit tests for our service methods. We are going to write some tests using Junit and Mockito. Before we get started, let me introduce a couple of new test-specific annotations we will be using.

  • @RunWith: In Junit architecture, a runner class is responsible for running tests. If you want to replace the default built-in with another implementation, use this annotation on top of the test class. Let's use it to let MockitoJUnitRunner run our tests. MockitoJUnitRunner makes the process of injecting mock version of dependencies much easier.

  • @InjectMocks: Put this before the main class you want to test. DependenciesĀ annotatedĀ withĀ @MockĀ will be injected to this class.Ā 

  • @Mock: Put this annotation before a dependency that's been added as a test class property. It will create a mock version of the dependency, and inject them into the class you are about to test.Ā  Ā @InjectMocksĀ  annotation.Ā 

We will also use theĀ Mockito.when(...).thenReturn(...)Ā methods to define fixed responses for when the methods of the mocked dependencies are called.Ā 

Ā Now that we have loosely coupled components (repositories, services, and controllers), we can take advantage of mocking, and unit test our Ā WatchlsitService. We will use Mockito to provide and inject mock objects into Ā WatchlistService.Ā We will use these annotations:

Ā Go to the src/test/java folder. Inside the package, create a new class called Ā WatchlistServiceTestĀ  with these test methodsĀ getWatchlistItemsShoiuldReturnAllItems.

package com.openclassrooms.watchlist.service;

import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

import com.openclassrooms.watchlist.domain.WatchlistItem;
import com.openclassrooms.watchlist.repository.WatchlistRepository;

@RunWith(MockitoJUnitRunner.class)
public class WatchlistServiceTest {
	
	@InjectMocks
	private WatchlistService watchlistService;
	
	@Mock
	private WatchlistRepository watchlistRepositoryMock;
	
	@Mock
	private MovieRatingService movieRatingServiceMock;
	
	@Test
	public void testGetWatchlistItemsReturnsAllItemsFromRepository() {
		
		//Arrange
		WatchlistItem item1 = new WatchlistItem("Star Wars", "7.7", "M" , "" , 1);
		WatchlistItem item2 = new WatchlistItem("Star Treck", "8.8", "M" , "" , 2);
		List<WatchlistItem> mockItems = Arrays.asList(item1, item2);
		
		when(watchlistRepositoryMock.getList()).thenReturn(mockItems);
		
		//Act
		List<WatchlistItem> result = watchlistService.getWatchlistItems();
		
		//Assert
		assertTrue(result.size() == 2);
		assertTrue(result.get(0).getTitle().equals("Star Wars"));
		assertTrue(result.get(1).getTitle().equals("Star Treck"));
	}
}

You can run it in Eclipse as a JUnit test and watch it pass.

Let's also run another unit test for theĀ Ā getWatchlistItem(..)Ā  method to make sure the rating that comes from the OMDb API overrides the rating value of the Watchlist items already stored in the app.Ā 

Here's the code we've gone over:

package com.openclassrooms.watchlist;



import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

import java.util.Arrays;
import java.util.List;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

import com.openclassrooms.watchlist.domain.WatchlistItem;
import com.openclassrooms.watchlist.repository.WatchlistRepository;
import com.openclassrooms.watchlist.service.MovieRatingService;
import com.openclassrooms.watchlist.service.WatchlistService;


@RunWith(MockitoJUnitRunner.class)
public class WatchlistServiceTest {

	
	@Mock
	private WatchlistRepository watchlistRepositoryMock;
	
	@Mock
	private MovieRatingService movieRatingServiceMock;
	
	@InjectMocks
	private WatchlistService watchlistService;
	
	@Test
	public void testGetWatchlistItemsReturnsAllItemsFromRepository() {
		
		//Arrange
		WatchlistItem item1 = new WatchlistItem("Star Wars", "7.7", "M" , "" , 1);
		WatchlistItem item2 = new WatchlistItem("Star Treck", "8.8", "M" , "" , 2);
		List<WatchlistItem> mockItems = Arrays.asList(item1, item2);
		
		when(watchlistRepositoryMock.getList()).thenReturn(mockItems);
		
		//Act
		List<WatchlistItem> result = watchlistService.getWatchlistItems();
		
		//Assert
		assertTrue(result.size() == 2);
		assertTrue(result.get(0).getTitle().equals("Star Wars"));
		assertTrue(result.get(1).getTitle().equals("Star Treck"));
	}
	
	@Test
	public void testGetwatchlistItemsRatingFormOmdbServiceOverrideTheValueInItems() {
		
		//Arrange
		WatchlistItem item1 = new WatchlistItem("Star Wars", "7.7", "M" , "" , 1);
		List<WatchlistItem> mockItems = Arrays.asList(item1);
		
		when(watchlistRepositoryMock.getList()).thenReturn(mockItems);	
		when(movieRatingServiceMock.getMovieRating(any(String.class))).thenReturn("10");
		
		//Act
		List<WatchlistItem> result = watchlistService.getWatchlistItems();
		
		//Assert
		assertTrue(result.get(0).getRating().equals("10"));
	}
}

Ta-da!

Try it yourself challenge!

Now that you know how to write unit tests using Mockito. try writing more tests for different scenarios and different methods of Ā WatchlistService. Start by writing a test for theĀ Ā  getWatchlistItemsSize()Ā  method.Ā 

Fix current controller tests

I’m not sure if you noticed it or not, but adding dependency injection to our application broke the Ā WatchlistControllerTestĀ we created earlier. Let's fix it!Ā 

Both of our test methods are failing because, while running tests, no Ā WatchlistServiceĀ Spring bean can be found in the context to be injected to Ā WatchlsitController.Ā  Fix that by adding Ā WatchlistServiceĀ  as a test class property annotated with Ā @MockBean.

@MockBean
private WatchlistService watchlistService;

testSubmitWatchlistItemForm()Ā  method test is still failing. That's because the submission handler methodĀ nowĀ expects a validĀ WatchlistItemĀ to be submitted. Fix that by passing form fields with valid values after Ā post(..)Ā  method. Your test class should look like this:

package com.openclassrooms.watchlist;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

import com.openclassrooms.watchlist.service.WatchlistService;

@WebMvcTest
@RunWith(SpringRunner.class)
public class WatchlistControllerTest {
	
	@Autowired
	MockMvc mockMvc;
	
	@MockBean
	WatchlistService watchlistService;
	
	@Test
	public void testShowWatchlistItemForm() throws Exception {
		
		mockMvc.perform(get("/watchlistItemForm"))
		.andExpect(status().is2xxSuccessful())
		.andExpect(view().name("watchlistItemForm"))
		.andExpect(model().size(1))
		.andExpect(model().attributeExists("watchlistItem"));
	}
	
	@Test
	public void testSubmitWatchlistItemForm() throws Exception {
		mockMvc.perform(post("/watchlistItemForm")
				.param("title", "Top Gun")
				.param("rating", "5.5")
				.param("priority", "L"))
		.andExpect(status().is3xxRedirection())
		.andExpect(redirectedUrl("/watchlist"));
	}
}

Annnd it's fixed!Ā  Bravo! šŸ‘

Let’s recap!

In this chapter, you learned a few key terms:

  • Tightly coupled components mean that your implementation doesn't allow you to replace dependencies with mocks.Ā  This is not a good practice!Ā 

  • Loosely coupled components mean that a single class isn't linked to any particular implementation of its dependencies.Ā  This means it's easy to replace your dependencies with mocks!Ā 

  • Dependency injection focuses on declaring dependencies, creating instances of them outside of your implementation, then passing them to your code.

  • Spring beans are classes that are managed by Spring dependency injection framework.Ā 

You should feel comfortable refactoring your code to create loosely coupled components - in fact, for any new code, make sure you keep everything loosely coupled and follow the law of dependency injection! šŸ˜‰

Now, let's look more closely at those Spring beans! ā˜•ļø

Ever considered an OpenClassrooms diploma?
  • Up to 100% of your training program funded
  • Flexible start date
  • Career-focused projects
  • Individual mentoring
Find the training program and funding option that suits you best