23 Data Testing How to Use Spring Testing Data Access Layer Components

23 Data Testing - How to Use Spring Testing Data Access Layer Components #

As the final part of the course, starting from this lesson, we will discuss the testing solutions provided by Spring. Testing is a challenging aspect of web applications and is often an overlooked set of technologies. When an application involves interactions between data layers, service layers, web layers, and various external services, in addition to unit testing the components of each layer, we also need to thoroughly introduce integration testing to ensure the correctness and stability of the services.

Testing Solutions in Spring Boot #

Similar to Spring Boot 1.x, Spring Boot 2.x also provides a spring-boot-starter-test component for testing.

In Spring Boot, we can integrate this component by adding the following dependencies to the pom file:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
        
<dependency>
    <groupId>org.junit.platform</groupId>
    <artifactId>junit-platform-launcher</artifactId>
    <scope>test</scope>
</dependency>

The last dependency is used to import components related to JUnit.

Then, by examining the dependency tree of the spring-boot-starter-test component through Maven, we can get the following component dependency diagram:

Drawing 1.png

Dependency diagram of the spring-boot-starter-test component

In Lesson “Case-Driven: How to Analyze a Spring Web Application?”, we mentioned that Spring Boot makes coding, configuration, deployment, and monitoring work easier. In fact, Spring Boot can also make testing work easier.

From the above diagram, we can see that we introduce a series of components to initialize the test environment in the build path of the code project. These components include JUnit, JSON Path, AssertJ, Mockito, Hamcrest, etc. Here, it is necessary for us to explain these components in detail.

  • JUnit: JUnit is a very popular unit testing framework based on the Java language, which is mainly used as the foundation of our course’s testing framework.
  • JSON Path: Similar to XPath in XML documents, JSON Path expressions are typically used to retrieve or set data in JSON files.
  • AssertJ: AssertJ is a powerful fluent assertion tool that adheres to the 3A core principles: Arrange (initialize test objects or prepare test data) -> Act (invoke the method under test) -> Assert (perform assertions).
  • Mockito: Mockito is a popular mocking framework in the Java world, which mainly uses a concise API to simulate operations. We will use this framework extensively during integration testing.
  • Hamcrest: Hamcrest provides a set of matchers, where each matcher is designed to perform specific comparison operations.
  • JSONassert: JSONassert is a specialized assertion framework for JSON.
  • Spring Test & Spring Boot Test: Testing tools provided for the Spring and Spring Boot frameworks.

The dependencies of these components are automatically imported, so we don’t need to make any changes. However, for certain specific scenarios, we need to manually import some components to meet testing requirements, such as embedding the H2 embedded relational database dedicated to testing scenarios.

Testing Spring Boot Applications #

Next, we will initialize the test environment for a Spring Boot application and introduce the methods and techniques to perform unit testing within a single service.

After importing the spring-boot-starter-test dependency, we can use its provided features to handle complex testing scenarios.

Initializing the Test Environment #

For a Spring Boot application, we know that the main() entry point in its Bootstrap class starts the Spring container through the SpringApplication.run() method. The following CustomerApplication class is a typical Spring Boot startup class:

@SpringBootApplication
public class CustomerApplication {

        public static void main(String[] args) {
            SpringApplication.run(CustomerApplication.class, args);
        }
}

For the above Bootstrap class, we can verify whether the Spring container can start normally by writing test cases.

To add test cases, it is necessary to organize the code structure. After organizing it, the basic directory structure of the code in the customer-service project is as shown in the following image:

Drawing 3.png

Directory structure of the customer-service project

Based on the default style of Maven, we will add various test case code and configuration files under the src/test/java and src/test/resources packages, as shown in the above image.

Open the ApplicationContextTests.java file in the above image, and we can get the following test case code:

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.junit4.SpringRunner;
 
@SpringBootTest
@RunWith(SpringRunner.class)
@Test
public void testConstructor() {
    CustomerTicket ticket = new CustomerTicket(1L, "1234567890");
    assertNotNull(ticket);
}

这个测试用例通过传入长度为10的字符串验证了构造函数的规则,如果长度不等于10,会抛出异常。

接下来,我们来看看如何对异常场景进行测试。

例如,如果我们在创建 CustomerTicket 对象时传入 null 的 accountId,会抛出 IllegalArgumentException 异常。我们可以通过如下测试用例来验证是否会抛出异常:

@Test(expected = IllegalArgumentException.class)
public void testConstructorAccountIdIsNull() {
    CustomerTicket ticket = new CustomerTicket(null, "1234567890");
}

这个测试用例使用了 @Test 注解的 expected 属性来指定期望的异常类型,如果创建 CustomerTicket 对象时抛出了 IllegalArgumentException 异常,则表示测试通过。

通过上述测试用例,我们可以验证 CustomerTicket 类的构造函数的正确性。在实际的开发中,我们应该编写更多的测试用例来覆盖各种不同的情况,确保代码的健壮性。

总结 #

在这一讲中,我们主要讲解了如何使用 SpringBootTest 注解和 RunWith 注解进行 Spring Boot 单元测试。同时,我们也介绍了如何通过 JUnit 框架来执行测试用例,并举了两个例子进行说明。

在实际的项目中,我们还需补充测试用例的配置信息、测试用例中相关方法的间接测试、测试用例的异常捕捉等内容。

@RunWith(SpringRunner.class)

public class CustomerTicketTests {


    private static final String ORDER_NUMBER = "Order00001";


    @Test

    public void testOrderNumberIsExactly10Chars() throws Exception {

        CustomerTicket customerTicket = new CustomerTicket(100L, ORDER_NUMBER);


                assertThat(customerTicket.getOrderNumber().toString()).isEqualTo(ORDER_NUMBER);

    }

}

After executing this unit test, we can see the execution process and results.

These unit test cases only demonstrate the most basic testing methods. We will expand and evolve on these methods in subsequent testing mechanisms.

Use the @DataJpaTest Annotation to Test Data Access Components #

Data needs to be persisted, so next we will discuss how to test the repository layer from the perspective of data persistence and introduce the @DataJpaTest annotation for testing JPA data access technology.

The @DataJpaTest annotation automatically injects various repository classes and initializes an in-memory database and a data source for accessing the database. In test scenarios, we generally use H2 as the in-memory database and use MySQL for data persistence. Therefore, we need to import the following Maven dependencies:

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

On the other hand, we need to prepare database DDL for database table initialization and provide DML scripts for data initialization. The schema-mysql.sql and data-h2.sql scripts respectively serve as the DDL and DML.

In the schema-mysql.sql of the customer-service project, the creation statement for the CUSTOMER table is included, as shown in the following code:

DROP TABLE IF EXISTS `customerticket`;

create table `customerticket` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `account_id` bigint(20) not null,
    `order_number` varchar(50) not null,
    `description` varchar(100) not null,
    `create_time` timestamp not null DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`)
);

In the data-h2.sql, we insert a test data that will be used, as shown in the following code:

INSERT INTO customerticket (`account_id`, `order_number`,`description`) values (1, 'Order00001', ' DemoCustomerTicket1');

Next is to provide the specific repository interface. Let’s review the definition of the CustomerTicketRepository interface with the following code:

public interface CustomerTicketRepository extends JpaRepository<CustomerTicket, Long> {

    List<CustomerTicket> getCustomerTicketByOrderNumber(String orderNumber);

}

Here, there is a method-based query, getCustomerTicketByOrderNumber, which retrieves a CustomerTicket based on the order number.

Based on the CustomerRepository mentioned above, we can write the following test cases:

@RunWith(SpringRunner.class)
@DataJpaTest
public class CustomerRepositoryTest {

    @Autowired
    private TestEntityManager entityManager;

    @Autowired
    private CustomerTicketRepository customerTicketRepository;

    @Test
    public void testFindCustomerTicketById() throws Exception {             
        this.entityManager.persist(new CustomerTicket(1L, "Order00001", "DemoCustomerTicket1", new Date()));

        CustomerTicket customerTicket = this.customerTicketRepository.getOne(1L);
        assertThat(customerTicket).isNotNull();
        assertThat(customerTicket.getId()).isEqualTo(1L);
    }

    @Test
    public void testFindCustomerTicketByOrderNumber() throws Exception {    
        String orderNumber = "Order00001";

        this.entityManager.persist(new CustomerTicket(1L, orderNumber, "DemoCustomerTicket1", new Date()));
        this.entityManager.persist(new CustomerTicket(2L, orderNumber, "DemoCustomerTicket2", new Date()));

        List<CustomerTicket> customerTickets = this.customerTicketRepository.getCustomerTicketByOrderNumber(orderNumber);
        assertThat(customerTickets).size().isEqualTo(2);
        CustomerTicket actual = customerTickets.get(0);
        assertThat(actual.getOrderNumber()).isEqualTo(orderNumber);
    }

    @Test
    public void testFindCustomerTicketByNonExistedOrderNumber() throws Exception {              
        this.entityManager.persist(new CustomerTicket(1L, "Order00001", "DemoCustomerTicket1", new Date()));
        this.entityManager.persist(new CustomerTicket(2L, "Order00002", "DemoCustomerTicket2", new Date()));

        List<CustomerTicket> customerTickets = this.customerTicketRepository.getCustomerTicketByOrderNumber("Order00003");
        assertThat(customerTickets).size().isEqualTo(0);
    }

}

Here, we use the @DataJpaTest annotation to inject the CustomerRepository implementation. At the same time, we also pay attention to the core testing component TestEntityManager, which is similar to using the real CustomerRepository to complete data persistence, providing a mechanism for isolating data from the environment.

After executing these test cases, we need to pay attention to the console log output. The core log is shown below (simplified for display):

Hibernate: drop table customer_ticket if exists
Hibernate: drop sequence if exists hibernate_sequence
Hibernate: create sequence hibernate_sequence start with 1 increment by 1
Hibernate: create table customer_ticket (id bigint not null, account_id bigint, create_time timestamp, description varchar(255), order_number varchar(255), primary key (id))
Hibernate: create table localaccount (id bigint not null, account_code varchar(255), account_name varchar(255), primary key (id))
…
Hibernate: call next value for hibernate_sequence
Hibernate: call next value for hibernate_sequence
Hibernate: insert into customer_ticket (account_id, create_time, description, order_number, id) values (?, ?, ?, ?, ?)
Hibernate: insert into customer_ticket (account_id, create_time, description, order_number, id) values (?, ?, ?, ?, ?)
Hibernate: select customerti0_.id as id1_0_, customerti0_.account_id as account_2_0_, customerti0_.create_time as create_t3_0_, customerti0_.description as descript4_0_, customerti0_.order_number as order_nu5_0_ from customer_ticket customerti0_ where customerti0_.order_number=?
…
Hibernate: drop table customer_ticket if exists
Hibernate: drop sequence if exists hibernate_sequence

From the above logs, we can see the effects of executing various SQL statements. At this point, you can also modify these test cases and observe the results of execution.

Summary and Preview #

Testing is an independent technical system that developers need to pay sufficient attention to and put into practice, especially for testing web applications. In this lesson, based on Spring Boot, we provided a complete testing method and core annotations, and explained the testing process of data access components based on relational databases.

Here is a question for you to think about: when using Spring Boot to execute test cases, how can we complete the testing process of data access based on an in-memory database? Feel free to leave a comment and interact with me~

After introducing database access testing, in Lesson 24, we will discuss how to test web service layer components.