Detailed Steps for Testing with Spring Boot

Introduction

In my first and second jobs after graduation, I was primarily involved in code migration and refactoring (switching languages). The first company was migrating from PHP to Java, and now I am in a department migrating from Clojure to Java.

Refactoring is a task that can be completed by one person without needing to interface with clients, allowing for a fully immersed process. Once familiar with the existing business and the input and output parameters for external interactions, one can migrate and redesign any logic.

I care deeply about my code quality during refactoring, as well as how daily changes impact the online business.

Verifying business functionality by starting the project locally or in a testing environment is undoubtedly a time-wasting process. Therefore, having meaningful test cases that allow for automated testing before every code merge gives me peace of mind.

In the past, when writing test cases, I was rather rigid. I always focused on the code logic to write test cases, thinking about what data could cover all branches, leading to the need to modify test cases whenever there was a change.

Over time, I felt that writing test cases was a waste of time and lacked meaning. However, since I started using Spock (BDD), I have moved beyond the misconception of covering branches through data. The business we develop is not static; it is impossible for every method to be 100% covered and never change. As long as we can cover most scenarios with test cases, that is sufficient.

Testing using @SpringBootTest(classes=XxxApplication.class) is cumbersome and increases the runtime of test cases.

Through my understanding of Spring Test, I have streamlined and encapsulated the testing tools I commonly use.

These include:

  • Minimized Spring container environment

  • Automatically loading SQL based on test methods

  • Annotations to generate truncate SQL from ORM entity classes

  • Annotations for loading mybatis-plus-mapper as needed

  • Encapsulation of assertion utility classes

Testing Support

Introduction

The testing environment supports both JUnit 5 and Spock testing frameworks, abstracting related annotations and plugins for integration testing based on Spring Boot.

Unlike common usage, although @SpringBootTest is used, automatic configuration is disabled (it will not scan which AutoConfiguration to use), and it will not scan all beans in the current project.

Bean loading is based on the principle of minimization; only the necessary beans are loaded, while the unused ones are not.

@SpringBootTest – Fun Fact

Using @SpringBootTest will start a complete container each time.

When we have many AutoConfigurations and beans in the project, the cost of starting a container is significant. Spring Test has optimized this by caching the container, involving classes such as DefaultCacheAwareContextLoaderDelegate and MergedContextConfiguration, with the cache decision based on the hashcode of MergedContextConfiguration. The code is as follows:

 /**  * Generate a unique hash code for all properties of this  * {@code MergedContextConfiguration} excluding the  * {@linkplain #getTestClass() test class}.  */ @Override public int hashCode() {    int result = Arrays.hashCode(this.locations);    result = 31 * result + Arrays.hashCode(this.classes);    result = 31 * result + this.contextInitializerClasses.hashCode();    result = 31 * result + Arrays.hashCode(this.activeProfiles);    result = 31 * result + Arrays.hashCode(this.propertySourceLocations);    result = 31 * result + Arrays.hashCode(this.propertySourceProperties);    result = 31 * result + this.contextCustomizers.hashCode();    result = 31 * result + (this.parent != null ? this.parent.hashCode() : 0);    result = 31 * result + nullSafeClassName(this.contextLoader).hashCode();    return result; }

(Swipe left to view the complete code)

If the Profile, Application above @SpringBootTest, context-related initializers, or custom PropertySources change, a new container will be created.

These changes are relatively infrequent; however, this.classes (the class of all objects in the container) is the most likely to change. During testing, we inevitably mock objects. If two test cases mock different objects or one mocks and the other does not, their containers will start separately.

Moreover, if the container is reused, it means that @MockBean and @SpyBean objects are also reused. The mock logic of different test cases will accumulate on the mock objects. Running them individually is fine, but when executing all cases, some will fail.

Annotations

pers.zengsx.toolkit.test.sdk.base.LiteTest

Defines a minimized container (thus it is a one-time container; each test case class is a separate container, ensuring each test case class is in an independent environment), and automatic bean loading is disabled. The included capabilities are: Plugins (explained later):

  • Automatically read and load SqlFile

  • Generate Truncate Table SQL based on entities

Services generally have custom components, such as DAO, CACHE, LOCK, etc. In the test-example project, LiteTest has been further encapsulated to add plugins support; details are provided later.

Example as follows:

 package pers.zengsx.toolkit.test.example;  import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Import; import pers.zengsx.toolkit.test.example.custom.ExampleLiteTest; import pers.zengsx.toolkit.test.example.custom.Plugins; import pers.zengsx.toolkit.test.example.dao.IUserDAO; import pers.zengsx.toolkit.test.example.dao.impl.UserDAOImpl; import pers.zengsx.toolkit.test.example.dao.mappers.IUserMapper; import pers.zengsx.toolkit.test.example.model.User; import pers.zengsx.toolkit.test.sdk.plugin.truncate_table.TruncateTable; import pers.zengsx.toolkit.test.sdk.registrar.mybatis.LoadMybatisMapper; import pers.zengsx.toolkit.test.sdk.utils.AssertHelper;  @ExampleLiteTest(Plugins.DAO) @Import(UserDAOImpl.class) @LoadMybatisMapper(classes = IUserMapper.class) public class FirstExampleTest {      @Autowired     IUserDAO userDAO;      @Test     @TruncateTable(mpEntityClasses = User.class)     void test() {         AssertHelper.assertEqualsByJson("{\"id\":1,\"name\":\"mr.zeng\",\"nickname\":\"seven\"}", userDAO.getById(1));         AssertHelper.assertEqualsByJson(null, userDAO.getById(2));     }   }

(Swipe left to view the complete code)

The above example enables DAO components, and SQL initialization data is automatically loaded through the AUTO_LOAD_SQL component, testing the MyBatis query.

pers.zengsx.toolkit.test.sdk.base.LiteWebTest

Increased MockMvc support based on LiteTest

pers.zengsx.toolkit.test.example.custom.ExampleLiteWebTest adds custom support for the project on top of LiteWebTest.

Example as follows:

 package pers.zengsx.toolkit.test.example;  import lombok.SneakyThrows; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import org.springframework.test.web.servlet.MockMvc; import org.springframework.web.util.NestedServletException; import pers.zengsx.toolkit.test.example.controller.TestController; import pers.zengsx.toolkit.test.example.custom.ExampleLiteWebTest; import pers.zengsx.toolkit.test.example.dao.IUserDAO;  import static org.hamcrest.Matchers.containsString; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;  @ExampleLiteWebTest @Import(TestController.class) public class FirstWebExampleTest {      @Autowired     MockMvc mockMvc;      @MockBean     IUserDAO userDAO;      @Test     @SneakyThrows     void test() {         mockMvc.perform(get("/test/HelloWorld"))                 .andDo(print())                 .andExpect(status().isOk())                 .andExpect(content().string(containsString("HelloWorld")));          assertThrows(NestedServletException.class, () -> mockMvc.perform(get("/test/throwError")));     } }

(Swipe left to view the complete code)

pers.zengsx.toolkit.test.sdk.registrar.mybatis.LoadMybatisMapper

If you want to use IXXXMapper (mybatis-plus mapper), you cannot directly import it using @Import(IXXXMapper.class) (even with the @Mapper annotation). Special handling is required to support this.

pers.zengsx.toolkit.test.sdk.plugin.truncate_table.TruncateTable

When the @TruncateTable(mpEntityClasses=XxxAutogenMybatisPO.class) annotation is used on a test case, it will read the @TableName annotation on XxxAutogenMybatisPO.class, generate truncate table SQL, and execute it.

Plugins

TruncateTable – ORM Entity Truncate

Described above @TruncateTable.

AutoLoadSqlFile – Automatically Load SQL File (currently not supported in Spock test cases)

Before executing the test case, it will search for a SQL file named ClassName_MethodName.sql in resources/it-sql and load it into the database.

Spock

Introduction

Writing test cases in Spock is much more convenient syntactically than in JUnit. Aside from a few differences in annotations/syntax, the rest is quite similar.

Below are just two examples; the rest can be explored on your own.Example as follows (my favorite is the support for multi-line strings, which allows for direct JSON comparison):

 package pers.zengsx.toolkit.test.example  import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Import import org.springframework.test.context.jdbc.Sql import pers.zengsx.toolkit.test.example.custom.ExampleLiteTest import pers.zengsx.toolkit.test.example.custom.Plugins import pers.zengsx.toolkit.test.example.dao.IUserDAO import pers.zengsx.toolkit.test.example.dao.impl.UserDAOImpl import pers.zengsx.toolkit.test.example.dao.mappers.IUserMapper import pers.zengsx.toolkit.test.example.model.User import pers.zengsx.toolkit.test.sdk.plugin.truncate_table.TruncateTable import pers.zengsx.toolkit.test.sdk.registrar.mybatis.LoadMybatisMapper import pers.zengsx.toolkit.test.sdk.utils.AssertHelper import spock.lang.Specification import spock.lang.Unroll  @Unroll @ExampleLiteTest(Plugins.DAO) @Import(UserDAOImpl.class) @LoadMybatisMapper(classes = IUserMapper.class) class FirstExampleGTest extends Specification {      @Autowired     IUserDAO userDAO      @Sql(scripts = "classpath:it-sql/FirstExampleTest_test.sql")     @TruncateTable(mpEntityClasses = User.class)     def "test"() {         expect:         AssertHelper.assertEqualsByJson('''{         "id":1,         "name":"mr.zeng",         "nickname":"seven"         }''', userDAO.getById(1));          AssertHelper.assertEqualsByJson(null, userDAO.getById(2));     }  }

(Swipe left to view the complete code)

GitHub test toolkit address:

https://github.com/yuanzessrs/test-toolkit

End

Detailed Steps for Testing with Spring Boot

Link: https://www.cnblogs.com/yuanzessrs/p/16488567.html

This article is reproduced with permission from 51Testing. The text contained in the reproduced article is sourced from the author. If there are any issues with the content or copyright, please contact 51Testing for removal.

Past Reviews

Skill checklist for testers: job requirements for a monthly salary of 35K

In the wave of layoffs in large companies, what is the future for testers?

Common bug summaries and reproduction ideas

Detailed Steps for Testing with Spring BootClick “Read the original text” to recharge together!

Leave a Comment