• 15 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 03/07/2020

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! ☕️

Exemple de certificat de réussite
Exemple de certificat de réussite