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")));
}
}
|