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.Ā
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! š
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.
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!
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.Ā
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! š
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! āļø