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