Enterprise Spring boot testing: Part 3 ( Service test cases, Dependency mocking )
In this part, we will see about the service layer for our web application and how to write the tests for the service layer by using mocking of the dependencies.
If you are new here, this is a continuation of the multi-part Spring boot testing series. You can see the other links below:
- Enterprise Spring boot testing: Part 1 ( Significance, Types of tests, libraries, project setup )
- Enterprise Spring boot testing: Part 2 ( General concepts, Domain testing, Unit test cases, Repository testing )
- You find the entire project on the Github Repository.
Service class
The service layer contains the business logic for the application and may contain dependencies that are required for the functionality of the class. In our case, we already have some methods created in the Domain. We will be creating wrapper methods in the service that will call the relevant domain methods and also use the repository for reading and saving the Customer object.
Let’s see the Service class below:
package com.microideation.tutorial.springtest.service; import com.microideation.tutorial.springtest.dictionary.CustomerStatus; import com.microideation.tutorial.springtest.domain.Customer; import com.microideation.tutorial.springtest.repository.CustomerRepository; import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Service; import java.util.NoSuchElementException; import java.util.Optional; @Service public class CustomerService { private final CustomerRepository customerRepository; public CustomerService(CustomerRepository customerRepository) { this.customerRepository = customerRepository; } public Customer getCustomer(@NotNull Long id) { return customerRepository .findById(id) .orElseThrow(() -> new NoSuchElementException("No customer found with id: " + id)); } public Customer registerCustomer(@NotNull Customer customer) { customer.setStatus(CustomerStatus.DISABLED); customer.validate(); return customerRepository.save(customer); } public Customer activate(@NotNull Long custId) { Customer customer = getCustomer(custId); customer.activate(); return customerRepository.save(customer); } public Customer deactivate(@NotNull Long custId) { Customer customer = getCustomer(custId); customer.deactivate(); return customerRepository.save(customer); } }
We have one dependency for the CustomerRepository
( injected using constructor injection ) that will be used for retrieving and saving the Customer
object.
There are 4 methods in the Service class:
- getCustomer() -> Tries to find the customer for the given id and if not found, throws
NoSuchElementException.
- registerCustomer() -> Validates the customer object received and calls the repository method for saving to the database.
- activate() -> Read the customer object based on the received id and update the status to ACTIVE and save.
- deactivate() -> Read the customer object based on the received id and update the status to DISABLED and save.
Service test class
We will create the CustomerServiceTest
class for testing the service methods. We will be mocking the CustomerRepository
dependency and its methods so that we can focus on the core business logic in the service methods.
CustomerServiceTest
Let’s create the service test as per the below code:
We need to annotate the class with @ExtendWith(MockitoExtension.class)
. This makes sure that the mocking support is enabled for the specific class.
package com.microideation.tutorial.springtest.customers; import com.microideation.tutorial.springtest.dictionary.CustomerStatus; import com.microideation.tutorial.springtest.domain.Customer; import com.microideation.tutorial.springtest.repository.CustomerRepository; import com.microideation.tutorial.springtest.service.CustomerService; import org.aspectj.lang.annotation.Before; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.AdditionalAnswers; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.util.Assert; import java.util.NoSuchElementException; import java.util.Optional; import static com.microideation.tutorial.springtest.customers.CustomerFixture.standardCustomer; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) public class CustomerServiceTest { @Mock private CustomerRepository customerRepository; private CustomerService customerService; @BeforeEach public void setup() { customerService = new CustomerService(customerRepository); } // Test 1 @Test public void givenGetCustomer_whenFound_thenReturnCustomer() { // Create the objet Customer customer = standardCustomer(); // Pass to mock when(customerRepository.findById(eq(customer.getId()))).thenReturn(Optional.of(customer)); // Call Customer result = customerService.getCustomer(customer.getId()); // Assertions Assertions.assertNotNull(result); Assertions.assertEquals(result.getId(),customer.getId()); // Verify call to mock verify(customerRepository,times(1)).findById(eq(customer.getId())); } // Test 2 @Test public void givenGetCustomer_whenNotFound_thenThrowNoSuchElementException() { // Set mock to return null when(customerRepository.findById(anyLong())).thenReturn(Optional.ofNullable(null)); // Assert exception Assertions.assertThrows(NoSuchElementException.class,() -> { customerService.getCustomer(10l); }); // Verify call to mock verify(customerRepository,times(1)).findById(anyLong()); } // Test 3 @Test public void givenRegisterCustomer_whenInValid_thenThrowException() { Customer customer = standardCustomer(); customer.setFirstname(null); // Assert exception Assertions.assertThrows(RuntimeException.class,() -> { customerService.registerCustomer(customer); }); // Verify that the save was not called verify(customerRepository,times(0)).findById(anyLong()); } // Test 4 @Test public void givenRegisterCustomer_whenValid_thenRegisterAndReturn() { // Create customer Customer customer = standardCustomer(); customer.setId(null); // Saved object Customer savedCustomer = standardCustomer(); // Mock the save call when(customerRepository.save(eq(customer))).thenReturn(savedCustomer); // Call Customer saved = customerService.registerCustomer(customer); // Assertions Assertions.assertNotNull(saved); Assertions.assertNotNull(saved.getId()); Assertions.assertEquals(saved.getStatus(), CustomerStatus.DISABLED); // Verify that the save was not called verify(customerRepository,times(1)).save(eq(customer)); } // Test 5 @Test public void givenActivate_whenSuccess_thenStatusShowActive() { // Build the customer Customer customer = standardCustomer(); // Return the customer object for id when(customerRepository.findById(eq(customer.getId()))).thenReturn(Optional.of(customer)); // Return the same object that is passed to the mock when(customerRepository.save(any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); // Call Customer updCustomer = customerService.activate(customer.getId()); // Assertions Assertions.assertNotNull(updCustomer); Assertions.assertEquals(updCustomer.getStatus(),CustomerStatus.ACTIVE); // Verify that the mock methods called verify(customerRepository,times(1)).findById(eq(customer.getId())); verify(customerRepository,times(1)).save(any()); } // Test 6 @Test public void givenDeactivate_whenSuccess_thenStatusShowDisabled() { // Build the customer Customer customer = standardCustomer(); // Return the customer object for id when(customerRepository.findById(eq(customer.getId()))).thenReturn(Optional.of(customer)); // Return the same object that is passed to the mock when(customerRepository.save(any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); // Call Customer updCustomer = customerService.deactivate(customer.getId()); // Assertions Assertions.assertNotNull(updCustomer); Assertions.assertEquals(updCustomer.getStatus(),CustomerStatus.DISABLED); // Verify that the mock methods called verify(customerRepository,times(1)).findById(eq(customer.getId())); verify(customerRepository,times(1)).save(any()); } }
Mocking
As indicated, we are more interested in the core functionality of the service layer and are not bothered about how the persistence layer ( repository ) works here. So we will be creating a mock of the CustomerRepository
and pass to the CustomerService
. This will allow us to manipulate the mock and intercept the calls to it for providing our custom or orchestrated response.
This is one of the advantages of constructor injection. We will be able to pass the dependencies for testing easily compared to when we had it injected using field injection ( @Autowired )
@Mock annotation
For creating a mock we annotate the field with @Mock
annotation.
@Mock
private CustomerRepository customerRepository;
@BeforeEach annotation
Also, we need to initialize the CustomerService
instance passing this mock. The initialization is done inside the method annotated with @BeforeEach
. This method will be called for each test case thereby initializing it each time.
We are using the same naming conventions as specified in the domain test cases. Also, the fixture is again used here for creating a dummy object that we will return to the repository mock method when its invoked
Method Mocking
We need to mock the methods in CustomerRepository
and have them return the response we need. It is done using stubbing.
Stubbing
This is the process of intercepting and orchestrating the mock object method call to return or behave according to our requirements.
Note how the mocking of the repository method is done in Test 1.
// Mock the findById call when(customerRepository.findById(eq(customer.getId()))).thenReturn(Optional.of(customer));
when() is a Mockito method that accepts a mock object’s method and specifies what needs to be returned ( or performed ) when it’s invoked in the current context. We also use the eq() method of Mockito to specify that the mock need to only invoke if the specified value is passed. There are other values also possible like anyLong()
, anyString()
etc that matches any value of the specified type.
Here we specify that when the customerRepository
( the mock instance ) findById method is called, then we need to return the custom object we created.
In Test 5, we are using a different variation of the when(), and instead of thenReturn()
, we use thenAnswer(AddtionalAnswers.returnsFirstArg())
. This is intended to return the original argument that was passed to the mock. Useful in the cases where we need to have the mock return the same object as passed.
Verification of mock interaction
After the mocking is done, we can invoke the actual service call and do the assertions.
Finally to verify that there was actually some interaction with our mock ( CustomerRepository
), we can use the Mockito.verify()
method. This is similar to the when() method, but here we are validating that there was interaction with the mock with specified values for the required number of times.
// Verify call to mock verify(customerRepository,times(1)).findById(eq(customer.getId()));
The remaining logic for the test cases ( assertions, validations, etc ) are similar to what we have seen in the Domain test cases in Part 2.
Footnotes
We covered the Service layer test cases and the concepts of mocking, verification of mock interactions, etc in this post. There are different variations of the mock method matchers and the return value configurations that you can play around with and validate.
In the next post, we will be covering web layer testing ( MockMVC, result validation ), etc. Enterprise Spring boot testing: Part 4 ( Controller, Web layer testing, Integration Testing, MockMvc )
1 Response
[…] Enterprise Spring boot testing: Part 3 ( Service test cases, Dependency mocking ) […]