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:

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:

  1. getCustomer() -> Tries to find the customer for the given id and if not found, throws NoSuchElementException.
  2. registerCustomer() -> Validates the customer object received and calls the repository method for saving to the database.
  3. activate() -> Read the customer object based on the received id and update the status to ACTIVE and save.
  4. 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 )

You may also like...

1 Response

  1. November 12, 2021

    […] Enterprise Spring boot testing: Part 3 ( Service test cases, Dependency mocking ) […]

Leave a Reply

Your email address will not be published. Required fields are marked *