目录

Spring Test

Spring Testing 对集成测试的支持和单元测试的最佳实践。Spring 团队提倡测试驱动开发 (TDD)。Spring 团队发现,正确使用控制反转 (IoC) 确实使单元测试和集成测试更容易(因为类上存在 setter 方法和适当的构造函数使它们更容易在测试中连接在一起,而不必建立服务定位器注册表和类似结构)。

单元测试

Mock 对象 org.springframework.mock

环境 org.springframework.mock.env 包含 Environment和PropertySource抽象的模拟实现。MockEnvironment 与 MockPropertySource对于为依赖于环境特定属性的代码开发容器外测试很有用。

Servlet API org.springframework.mock.web包包含一组全面的 Servlet API 模拟对象,可用于测试 Web 上下文、控制器和过滤器。这些模拟对象的目标是与 Spring 的 Web MVC 框架一起使用,并且通常比动态模拟对象(例如EasyMock)或替代 Servlet API 模拟对象(例如MockObjects)更方便使用。

Spring Web Reactive(反应式) org.springframework.mock.http.server.reactive包包含 WebFlux 应用程序的模拟实现ServerHttpRequest并ServerHttpResponse用于 WebFlux 应用程序。该 org.springframework.mock.web.server包包含一个ServerWebExchange依赖于这些模拟请求和响应对象的模拟。

集成测试

单元和集成测试支持以注释驱动的 Spring TestContext Framework 的形式提供。TestContext 框架与实际使用的测试框架无关,它允许在各种环境中进行测试,包括 JUnit、TestNG 等。

Spring 的集成测试支持有以下主要目标:

  • 管理测试之间的Spring IoC 容器缓存。
  • 提供测试夹具实例的依赖注入。
  • 提供适合集成测试的事务管理。
  • 提供特定于 Spring 的基类,帮助开发人员编写集成测试。

spring-boot-starter-test

  • spirng-boot-test
  • spring-test
  • junit-jupiter-api
  • junit-jupiter-engine
  • junit-jupiter-params
  • mockito-core
  • mockito-junit-jupiter
  • hamcrest
  • assertj-core
  • json-path
  • jsonassert
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<properties>
  <java.version>1.8</java.version>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  <spring-boot.version>2.3.3.RELEASE</spring-boot.version>
</properties><dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>
<dependencyManagement>
  <dependencies>
    <dependency>
      <!-- Import dependency management from Spring Boot -->
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-dependencies</artifactId>
      <version>${spring-boot.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>
  • 推荐测试用的 profile application-test.yaml

常用注解:

@RunWith(SpringRunner.class)

JUnit运行使用Spring的测试支持。SpringRunner是SpringJUnit4ClassRunner的新名字,这样做的目的仅仅是为了让名字看起来更简单一点。

@SpringBootTest 包含 JUnit 5 @ExtendWith({SpringExtension.class}) JUnit 前的版本 @RunWith(SpringRunner.class)

  • 如果您使用的是Junit版本<5,则必须使用@RunWith(SpringRunner.class)@RunWith(MockitoJUnitRunner.class)等。
  • 如果您使用的是Junit版本= 5,则必须使用@ExtendWith(SpringExtension.class)@ExtendWith(MockitoExtension.class)等。

@SpringBootTest

该注解为SpringApplication创建上下文并支持Spring Boot特性,其webEnvironment提供如下配置:

Mock-加载WebApplicationContext并提供Mock Servlet环境,嵌入的Servlet容器不会被启动。

RANDOM_PORT-加载一个EmbeddedWebApplicationContext并提供一个真实的servlet环境。嵌入的Servlet容器将被启动并在一个随机端口上监听。

DEFINED_PORT-加载一个EmbeddedWebApplicationContext并提供一个真实的servlet环境。嵌入的Servlet容器将被启动并在一个默认的端口上监听 (application.properties配置端口或者默认端口8080)。

NONE-使用SpringApplication加载一个ApplicationContext,但是不提供任何的servlet环境。

如果我们不指定classes属性,那么启动测试类时需要加载的Bean的数量和正常启动一次入口类(即有@SpringBootApplication注解的类)加载的 Bean 数量是一样的。 如果你的项目中有很多个 Bean

启动测试类的时候,就只会加载需要的 Bean 到上下文中,从而加快启动速度。

1
2
3
4
5
6
7
8
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes={HelloServiceImpl.class})  
public class Tests {  

    @Test  
    public void testHello() {  
        // ...  
    }  
}

@MockBean 在你的ApplicationContext里为一个bean定义一个Mockito mock。

@SpyBean 定制化Mock某些方法。使用@SpyBean除了被打过桩的函数,其它的函数都将真实返回。

@WebMvcTest 该注解被限制为一个单一的controller,需要利用@MockBean去Mock合作者(如service)。

DAO 层测试

测试Dao的时候为了防止引入脏数据使用注解@Transactional和@Rollback在测试完成后进行回滚。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class ScoreControllerTestNew {

    @Autowired
    private ModelMonitorMapper modelMonitorMapper;
    @Test
    @Rollback
    public void testDao() throws Exception {
        ModelMonitor modelMonitor = new ModelMonitor();
        modelMonitor.setModelProductId(Long.parseLong("5"));
        modelMonitor.setLogit(21.144779999999997);
        modelMonitor.setDerivedVariables("{\"debit_account_balance_code\":1.0,\"credit_consume_count\":1.0,\"debit_start_age\":1.0,\"debit_consume_sum_code\":1.0,\"age\":1.0}");
        modelMonitor.setScore("300");
        modelMonitor.setSrcData("{\"data\":{\"debit_account_balance_code\":40,\"credit_consume_count\":1,\"debit_start_age\":1,\"debit_consume_sum_code\":2,\"age\":38},\"modelProductId\":5}");
        int n = modelMonitorMapper.insert(modelMonitor);
        assertThat(n).as("检查数据是否成功插入").isEqualTo(0);
    }
}

@DataJpaTest

对于JPA,Repository进行测试的时候可以使用 @DataJpaTest 注解,有了这个注解,Spring在启动的时候就只会加载@Repository 相关的class。同样可以提高测试的效率。

1
2
3
4
5
6
7
dependencies {
  compile('org.springframework.boot:spring-boot-starter-data-jpa')
  compile('org.springframework.boot:spring-boot-starter-web')
  runtime('com.h2database:h2')
  testCompile('org.springframework.boot:spring-boot-starter-test')
  testCompile('org.junit.jupiter:junit-jupiter-engine:5.2.0')
}
1
2
3
4
5
interface UserRepository extends CrudRepository<UserEntity, Long> {
  UserEntity findByName(String name);
  @Query("select u from UserEntity u where u.name = :name")
  UserEntity findByNameCustomQuery(@Param("name") String name);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@ExtendWith(SpringExtension.class)
@DataJpaTest
@TestPropertySource(properties = {
        "spring.jpa.hibernate.ddl-auto=validate"
})
class UserEntityRepositoryTest {

  @Autowired private DataSource dataSource;
  @Autowired private JdbcTemplate jdbcTemplate;
  @Autowired private TestEntityManager entityManager;
  @Autowired private UserRepository userRepository;

  @Test
  void injectedComponentsAreNotNull(){
    assertThat(dataSource).isNotNull();
    assertThat(jdbcTemplate).isNotNull();
    assertThat(entityManager).isNotNull();
    assertThat(userRepository).isNotNull();
  }
}
1
compile('org.flywaydb:flyway-core')
1
2
3
4
5
6
7
8
@ExtendWith(SpringExtension.class)
@DataJpaTest
@TestPropertySource(properties = {
        "spring.jpa.hibernate.ddl-auto=validate"
})
class FlywayTest {
  //
}

@MyBatisTest

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
        <!--mybatis begin-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter-test</artifactId>
            <version>2.1.3</version>
            <scope>test</scope>
        </dependency>

application-test.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/store?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&allowMultiQueries=true&rewriteBatchedStatements=true&useAffectedRows=true
    username: root
    password: lhddemo
    driver-class-name: com.mysql.cj.jdbc.Driver
    maximum-pool-size: 12
    minimum-idle: 10
    idle-timeout: 500000
    max-lifetime: 540000
#mybatis
mybatis:
  mapper-locations: classpath:/mapper/*Mapper.xml
  type-aliases-package: com.ynthm.demo.mapper
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
1
2
3
4
5
6
@ActiveProfiles("test")
@MybatisTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class GoodsMapperTest {
    @Resource
    private GoodsMapper goodsMapper;

@MybatisPlusTest

@MybatisPlusTest 已经添加 @ExtendWith(SpringExtension.class)

1
compile group: 'com.baomidou', name: 'mybatis-plus-boot-starter-test', version: 'latest-version'
1
2
3
4
5
// @ActiveProfiles("test")
@MybatisPlusTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
// @MapperScan({"com.ztx.kol.api.mapper.**"})
public class BaseMapperTest {}

testcontainers 模拟数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<properties>
  	<java.version>1.8</java.version>
  	<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  	<testcontainers.version>1.14.3</testcontainers.version>
</properties>

<dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.testcontainers</groupId>
                <artifactId>testcontainers-bom</artifactId>
                <version>${testcontainers.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
</dependencyManagement>

Service 层测试

基于mock的隔离测试和基于真实数据(unitils-dbunit、testcontainers)的普通测试。

隔离测试主要引入 Mockito 通过 @Mock 和 @InjectMocks 两个注解来实现模拟与被模拟。

Mockito Most popular Mocking framework for unit tests written in Java

  • @Mock:模拟出一个Mock对象,对象是空的,需要指明对象调用什么方法,传入什么参数时,返回什么值
  • @InjectMocks:依赖@Mock对象的类,也即是被测试的类。@Mock出的对象会被注入到@InjectMocks对象中
1
2
// https://mvnrepository.com/artifact/org.mockito/mockito-core
testImplementation 'org.mockito:mockito-core:4.5.1'
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class TestEmployeeManager {

	@InjectMocks
	EmployeeManager manager;

	@Mock
	EmployeeDao dao;

	@Before
	public void init() {
		MockitoAnnotations.initMocks(this);
	}

	@Test
	public void getAllEmployeesTest()
	{
		List<EmployeeVO> list = new ArrayList<EmployeeVO>();
		EmployeeVO empOne = new EmployeeVO(1, "John", "John", "howtodoinjava@gmail.com");
		EmployeeVO empTwo = new EmployeeVO(2, "Alex", "kolenchiski", "alexk@yahoo.com");
		EmployeeVO empThree = new EmployeeVO(3, "Steve", "Waugh", "swaugh@gmail.com");

		list.add(empOne);
		list.add(empTwo);
		list.add(empThree);

		when(dao.getEmployeeList()).thenReturn(list);

		//test
		List<EmployeeVO> empList = manager.getEmployeeList();

		assertEquals(3, empList.size());
		verify(dao, times(1)).getEmployeeList();
	}

	@Test
	public void getEmployeeByIdTest()
	{
		when(dao.getEmployeeById(1)).thenReturn(new EmployeeVO(1,"Lokesh","Gupta","user@email.com"));

		EmployeeVO emp = manager.getEmployeeById(1);

		assertEquals("Lokesh", emp.getFirstName());
		assertEquals("Gupta", emp.getLastName());
		assertEquals("user@email.com", emp.getEmail());
	}

	@Test
	public void createEmployeeTest()
	{
		EmployeeVO emp = new EmployeeVO(1,"Lokesh","Gupta","user@email.com");

		manager.addEmployee(emp);

		verify(dao, times(1)).addEmployee(emp);
	}
}

测试 Web 层

@WebMvcTest 将测试范围缩小到仅 Web 层

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@SpringBootApplication
public class TestingWebApplication {
	public static void main(String[] args) {
		SpringApplication.run(TestingWebApplication.class, args);
	}
}
@Controller
public class HomeController {

	@RequestMapping("/")
	public @ResponseBody String greeting() {
		return "Hello, World";
	}
}

使用AssertJ(它提供assertThat()和其他方法)来表达测试断言。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@SpringBootTest
public class SmokeTest {

	@Autowired
	private HomeController controller;

	@Test
	public void contextLoads() throws Exception {
		assertThat(controller).isNotNull();
	}
}


@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class HttpRequestTest {

	@LocalServerPort
	private int port;

	@Autowired
	private TestRestTemplate restTemplate;

	@Test
	public void greetingShouldReturnDefaultMessage() throws Exception {
		assertThat(this.restTemplate.getForObject("http://localhost:" + port + "/",
				String.class)).contains("Hello, World");
	}
}

// 使用 @AutoConfigureMockMvc 无需不启动服务器。在这个测试中,启动了完整的 Spring 应用程序上下文,但没有服务器。
@SpringBootTest
@AutoConfigureMockMvc
public class TestingWebApplicationTest {

	@Autowired
	private MockMvc mockMvc;

	@Test
	public void shouldReturnDefaultMessage() throws Exception {
		this.mockMvc.perform(get("/")).andDo(print()).andExpect(status().isOk())
				.andExpect(content().string(containsString("Hello, World")));
	}
}
// @WebMvcTest 将测试范围缩小到仅 Web 层
@WebMvcTest
public class WebLayerTest {

	@Autowired
	private MockMvc mockMvc;

	@Test
	public void shouldReturnDefaultMessage() throws Exception {
		this.mockMvc.perform(get("/")).andDo(print()).andExpect(status().isOk())
				.andExpect(content().string(containsString("Hello, World")));
	}
}

@WebMvcTest(HomeController.class) 测试指定 Controller

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Controller
public class GreetingController {

	private final GreetingService service;

	public GreetingController(GreetingService service) {
		this.service = service;
	}

	@RequestMapping("/greeting")
	public @ResponseBody String greeting() {
		return service.greet();
	}
}
@Service
public class GreetingService {
	public String greet() {
		return "Hello, World";
	}
}
@WebMvcTest(GreetingController.class)
public class WebMockTest {

	@Autowired
	private MockMvc mockMvc;

	@MockBean
	private GreetingService service;

	@Test
	public void greetingShouldReturnMessageFromService() throws Exception {
		when(service.greet()).thenReturn("Hello, Mock");
		this.mockMvc.perform(get("/greeting")).andDo(print()).andExpect(status().isOk())
				.andExpect(content().string(containsString("Hello, Mock")));
	}
}