24 Service Testing How to Use Spring Testing Web Service Layer Components

24 Service Testing - How to Use Spring Testing Web Service Layer Components #

In Lesson 23, we introduced the methods for testing the data access layer. In this lesson, we will focus on testing the other two layers of the three-tier architecture: the service layer and the controller layer.

Unlike the data access layer, the components in these two layers depend on their next layer. That is, the service layer depends on the data access layer, and the controller layer depends on the service layer. Therefore, when testing these two layers, we will use different techniques and technologies.

Testing Configuration Information using Environment #

In the lesson “Custom Configuration: How to Create Custom Configuration Information?”, we discussed the implementation of custom configuration information. In a Spring Boot application, the service layer usually depends on the configuration file, so we also need to test the configuration information.

There are two ways to test configuration information: one depends on physical configuration files, and the other dynamically injects configuration information during testing.

The first testing approach is relatively simple. When adding configuration files in the src/test/resources directory, Spring Boot can read the configuration items in these files and apply them to test cases. Before discussing the specific implementation process, let’s first understand the Environment interface which is defined as follows:

public interface Environment extends PropertyResolver {

    String[] getActiveProfiles();

    String[] getDefaultProfiles();

    boolean acceptsProfiles(String... profiles);

}

In the above code, we can see that the main purpose of the Environment interface is to handle profiles, and its parent interface PropertyResolver is defined as follows:

public interface PropertyResolver {

    boolean containsProperty(String key);

    String getProperty(String key);

    String getProperty(String key, String defaultValue);

    <T> T getProperty(String key, Class<T> targetType);

    <T> T getProperty(String key, Class<T> targetType, T defaultValue);

    String getRequiredProperty(String key) throws IllegalStateException;

    <T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException;

    String resolvePlaceholders(String text);

    String resolveRequiredPlaceholders(String text) throws IllegalArgumentException;

}

Obviously, the purpose of PropertyResolver is to get the value of configuration attributes based on various configuration item keys.

Now, let’s assume that the application.properties file in the src/test/resources directory contains the following configuration item:

springcss.order.point = 10

Then, we can design the following test case:

@RunWith(SpringRunner.class)

@SpringBootTest

public class EnvironmentTests{

 

    @Autowired

    public Environment environment;

 

    @Test

    public void testEnvValue(){

        Assert.assertEquals(10, Integer.parseInt(environment.getProperty("springcss.order.point"))); 

    }

}

Here, we inject an Environment interface and use its getProperty method to retrieve the configuration information in the test environment.

In addition to setting properties in the configuration file, we can also use the @SpringBootTest annotation to specify the properties for testing. The sample code is as follows:

@RunWith(SpringRunner.class)

@SpringBootTest(properties = {" springcss.order.point = 10"})

public class EnvironmentTests{

 

    @Autowired

    public Environment environment;

 

    @Test

    public void testEnvValue(){

        Assert.assertEquals(10, Integer.parseInt(environment.getProperty("springcss.order.point"))); 

    }

}

Testing Service Layer with Mock #

As mentioned at the beginning of this lesson, the service layer depends on the data access layer. Therefore, when testing the service layer, we need to introduce a new technical system, which is the widely used Mock mechanism.

Next, let’s take a look at the basic concept of the Mock mechanism.

Mock Mechanism #

Mock means simulation. It can be used to isolate systems, components, or classes.

During the testing process, we usually focus on the functionality and behavior of the tested object itself. We only pay attention to the interaction between the tested object and its dependencies (such as whether the dependencies are called, when they are called, the parameters passed in the calls, the number and order of calls, as well as the returned results or exceptions that occur). We do not care about the specific details of how these dependent objects carry out the calls. Therefore, the Mock mechanism is used to replace the real dependent objects with Mock objects and simulate real scenarios to carry out the testing work.

The diagram below illustrates the use of Mock objects to test the dependency relationship:

Mock Objects and Dependency Relationship

From the diagram, we can see that in form, Mock involves mocking classes directly in test code and defining the behavior of Mock methods. Usually, the test code and the Mock code are placed together. Therefore, the logic of the test code can be easily reflected in the code of the test case.

Now let’s see how to use Mock to test the service layer.

Using Mock #

In lesson 23, we introduced the SpringBootTest.WebEnvironment.MOCK option in the @SpringBootTest annotation. This option is used to load the WebApplicationContext and provide a Mock Servlet environment, where the built-in Servlet container is not actually started. Next, we will demonstrate this testing approach for the service layer.

First, let’s take a look at a simple scenario where the CustomerTicketService class exists in customer-service:

@Service
public class CustomerTicketService {

    @Autowired
    private CustomerTicketRepository customerTicketRepository;

    public CustomerTicket getCustomerTicketById(Long id) {
        return customerTicketRepository.getOne(id);
    }
    ...
}

Here we can see that the above method simply performs a data query operation through the CustomerTicketRepository.

Obviously, when we perform integration testing on the CustomerTicketService mentioned above, we also need to provide a dependency on CustomerTicketRepository.

Next, let’s use the following code to demonstrate how to isolate the CustomerTicketRepository using the mocking mechanism:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
public class CustomerServiceTests {

    @MockBean
    private CustomerTicketRepository customerTicketRepository;

    @Test
    public void testGetCustomerTicketById() throws Exception {
        Long id = 1L;
        Mockito.when(customerTicketRepository.getOne(id))
            .thenReturn(new CustomerTicket(1L, 1L, "Order00001", "DemoCustomerTicket1", new Date()));

        CustomerTicket actual = customerTicketService.getCustomerTicketById(id);

        assertThat(actual).isNotNull();
        assertThat(actual.getOrderNumber()).isEqualTo("Order00001");
    }
}

First, we inject CustomerTicketRepository using the @MockBean annotation. Then, we use the when/thenReturn mechanism provided by the third-party mocking framework Mockito to mock the getCustomerTicketById() method in CustomerTicketRepository.

Of course, if you want to directly inject the real CustomerTicketRepository in the test case, you can use the SpringBootTest.WebEnvironment.RANDOM_PORT option in the @SpringBootTest annotation. The code example is as follows:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class CustomerServiceTests {

    @Autowired
    private CustomerTicketRepository customerTicketRepository;

    @Test
    public void testGetCustomerTicketById() throws Exception {
        Long id = 1L;
        CustomerTicket actual = customerTicketService.getCustomerTicketById(id);

        assertThat(actual).isNotNull();
        assertThat(actual.getOrderNumber()).isEqualTo("Order00001");
    }
}

After running the above code, the entire Spring Boot project will start on a random port, and the target data will be retrieved from the database for verification.

The example of the above integration test only includes dependencies on the Repository layer. However, sometimes a Service may simultaneously depend on Repository and other Service classes or components. Let’s go back to the CustomerTicketService class as shown below:

@Service
public class CustomerTicketService {

    @Autowired
    private OrderClient orderClient;

    private OrderMapper getRemoteOrderByOrderNumber(String orderNumber) {
        return orderClient.getOrderByOrderNumber(orderNumber);
    }
    ...
}

Here we can see that in this code, in addition to the CustomerTicketRepository dependency, it also depends on OrderClient.

Please note: The OrderClient in the above code is an implementation class accessed by the customer-service in order to access the order-service remotely through RestTemplate. The code is shown as follows:

@Component
public class OrderClient {
   
    @Autowired
    RestTemplate restTemplate;
   
    public OrderMapper getOrderByOrderNumber(String orderNumber) {
   
        ResponseEntity<OrderMapper> restExchange = restTemplate.exchange(
                "http://localhost:8083/orders/{orderNumber}", HttpMethod.GET, null,
                OrderMapper.class, orderNumber);
   
        OrderMapper result = restExchange.getBody();
   
        return result;
    }
}

CustomerTicketService doesn’t care about how OrderClient implements the remote access. Because for integration testing, it only cares about the result of the method call, so we will use the Mock mechanism to isolate OrderClient.

The test case code for the CustomerTicketService functionality is as follows. As you can see, we use the same testing approach.

@Test
public void testGenerateCustomerTicket() throws Exception {
    Long accountId = 100L;
    String orderNumber = "Order00001";
    
    Mockito.when(this.orderClient.getOrderByOrderNumber("Order00001"))
        .thenReturn(new OrderMapper(1L, orderNumber, "deliveryAddress"));
    
    Mockito.when(this.localAccountRepository.getOne(accountId))
        .thenReturn(new LocalAccount(100L, "accountCode", "accountName"));
    
    CustomerTicket actual = customerTicketService.generateCustomerTicket(accountId, orderNumber);
    
    assertThat(actual.getOrderNumber()).isEqualTo(orderNumber);
}

The provided test case demonstrates various methods of integration testing in the Service layer. They are already capable of meeting the needs of general scenarios.

Testing the Controller Layer #

Before testing the Controller layer, let’s provide a typical Controller class from the customer-service, as shown in the following code:

@RestController
@RequestMapping(value="customers")
public class CustomerController {
    
    @Autowired
    private CustomerTicketService customerTicketService;
    
    @PostMapping(value = "/{accountId}/{orderNumber}")
    public CustomerTicket generateCustomerTicket( @PathVariable("accountId") Long accountId,
            @PathVariable("orderNumber") String orderNumber) {
        CustomerTicket customerTicket = customerTicketService.generateCustomerTicket(accountId, orderNumber);
        return customerTicket;
    }
}

There are quite a few testing methods for the above Controller class, such as TestRestTemplate, @WebMvcTest annotation, and MockMvc. Let’s explain them one by one.

Using TestRestTemplate #

The TestRestTemplate provided by Spring Boot is very similar to RestTemplate, except that it is specifically used in testing environments.

If we want to use @SpringBootTest in the testing environment, we can directly use TestRestTemplate to test the remote access process. The sample code is as follows:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class CustomerController2Tests {

    @Autowired
    private TestRestTemplate testRestTemplate;

    @MockBean
    private CustomerTicketService customerTicketService;

    @Test
    public void testGenerateCustomerTicket() throws Exception {
        Long accountId = 100L;
        String orderNumber = "Order00001";
        
        given(this.customerTicketService.generateCustomerTicket(accountId, orderNumber))
            .willReturn(new CustomerTicket(1L, accountId, orderNumber, "DemoCustomerTicket1", new Date()));
        
        CustomerTicket actual = testRestTemplate.postForObject("/customers/" + accountId+ "/" + orderNumber, null, CustomerTicket.class);
        assertThat(actual.getOrderNumber()).isEqualTo(orderNumber);
}

}

In the above test code, first of all, we notice that the @SpringBootTest annotation specifies the web running environment with a random port by using SpringBootTest.WebEnvironment.RANDOM_PORT. Then, we use the TestRestTemplate to send an HTTP request and verify the result.

Note: The way we send requests using TestRestTemplate is exactly the same as RestTemplate. You can review the content of “Service Invocation: How to Consume RESTful Services Using RestTemplate?” for reference.

Using the @WebMvcTest Annotation #

Next in the test method, we introduce a new annotation, @WebMvcTest, which initializes the necessary Spring MVC infrastructure for testing the controller. Here is an example of testing the CustomerController class:

@RunWith(SpringRunner.class)

@WebMvcTest(CustomerController.class)

public class CustomerControllerTestsWithMockMvc {

 

    @Autowired

    private MockMvc mvc;

 

    @MockBean

    private CustomerTicketService customerTicketService;

 

    @Test

    public void testGenerateCustomerTicket() throws Exception {

        Long accountId = 100L;

        String orderNumber = "Order00001";

        given(this.customerTicketService.generateCustomerTicket(accountId, orderNumber))

                .willReturn(new CustomerTicket(1L, 100L, "Order00001", "DemoCustomerTicket1", new Date()));

 

        this.mvc.perform(post("/customers/" + accountId+ "/" + orderNumber).accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk());

    }

}

The key to the above code is the MockMvc utility class, so it is necessary to further explain it.

The basic methods provided by the MockMvc class fall into the following six categories, let’s look at them one by one.

  • Perform: Executes a RequestBuilder request, automatically performs the Spring MVC process and maps it to the corresponding controller for processing.
  • get/post/put/delete: Declares the HTTP request method, gets an HTTP request based on the URI template and URI variable values, supports GET, POST, PUT, DELETE, and other HTTP methods.
  • param: Adds request parameters, this method cannot be used when sending JSON data, instead, the @ResponseBody annotation should be used.
  • andExpect: Adds ResultMatcher validation rules to validate the correctness of the controller’s execution result by judging the returned data.
  • andDo: Adds ResultHandler result handlers, such as printing the result to the console for debugging.
  • andReturn: Finally, return the corresponding MvcResult and perform custom validation or asynchronous processing.

After executing this test case, from the output of the console logs, we can see that the whole process is equivalent to starting the CustomerController and performing a remote access, while the CustomerController uses the CustomerTicketService as a mock.

Clearly, the purpose of testing the CustomerController is to verify the format and content of its returned data. In the above code, we first define the JSON result that CustomerController will return, then simulate the entire process of an HTTP request using the perform, accept, and andExpect methods, and finally verify the correctness of the result.

Using the @AutoConfigureMockMvc Annotation #

Please note that the @SpringBootTest annotation cannot be used together with the @WebMvcTest annotation.

In the scenario of using the @SpringBootTest annotation, if we want to use the MockMvc object, we can introduce the @AutoConfigureMockMvc annotation.

By combining the @SpringBootTest and @AutoConfigureMockMvc annotations, the @AutoConfigureMockMvc annotation automatically configures the MockMvc class in the Spring context environment loaded by @SpringBootTest.

Here is an example of test code using the @AutoConfigureMockMvc annotation:

@RunWith(SpringRunner.class)

@SpringBootTest

@AutoConfigureMockMvc

public class CustomerControllerTestsWithAutoConfigureMockMvc {

 

    @Autowired

    private MockMvc mvc;

 

    @MockBean

    private CustomerTicketService customerTicketService;

 

    @Test

    public void testGenerateCustomerTicket() throws Exception {

        Long accountId = 100L;

        String orderNumber = "Order00001";

        given(this.customerTicketService.generateCustomerTicket(accountId, orderNumber))

                .willReturn(new CustomerTicket(1L, 100L, "Order00001", "DemoCustomerTicket1", new Date()));

 

        this.mvc.perform(post("/customers/" + accountId+ "/" + orderNumber).accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk());

    }

}

In the above code, we use the MockMvc utility class to simulate an HTTP request and verify the correctness of the Controller layer component based on the returned status.

Summary of Testing Annotations in Spring Boot #

Through the study of the previous content, I believe you have felt the core role played by various testing annotations in testing Spring Boot applications.

In the table below, we list some commonly used testing annotations and their descriptions.

图片8.png

Conclusion and Preview #

For a web application, testing the Service layer and the Web layer components is a core focus. In this lesson, we have tested the Service layer using the Mock mechanism and introduced three different methods to validate the Controller layer components.

Here is a question for you to think about: When testing a web application with Spring Boot, do you know what are the common testing annotations? Feel free to interact and communicate in the comments area.

After we finish discussing the testing components, we will move on to the last lesson of this specialization. In the conclusion, we will summarize Spring Boot and look forward to its future development.