Spring Test#
测试后端服务#
spring 服务的单元测试和普通 java 代码单元测试差不多,不过需要注意:
Spring 所有组件基于 ApplicationContext。需要注意 Context 管理
Spring 后端服务需要注意其启动端口号。
下面是一个简单的案例:
package com.example;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import org.junit.jupiter.api.Assertions;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import java.io.IOException;
/** spring web 测试, 使用随机端口启动 */
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class LivenessTest {
/** 通过 LocalServerPort 将启动端口号注入进来 */
@LocalServerPort
private Long port;
public void testLiveness() throws IOException {
// 尝试请求 liveness 接口, 断言其返回 http 200
var url = "http://localhost:" + port + "/liveness";
var client = new OkHttpClient();
var request = new Request.Builder().url(url).build();
var call = client.newCall(request);
var response = call.execute();
Assertions.assertEquals(200, response.code());
}
}
上面的代码需要注意:
class 上增加
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)启动后端服务. 随后便可以通过 http client 测试服务接口使用随机端口启动后,通过
@LocalServerPort得到端口号。
备注
如果不需要测试后端 http 接口,也可以将 SpringBootTest::webEnvironment 设置为其他值。
使其不通过端口号启动。
这种情况下,会自动寻找代码中的 @SpringBootApplication 启动入口, 这种情况下和服务正常启动的流程几乎一样。
这些细微差别体现在,单元测试情况下,很多周边的东西不会自动配置,例如 Scheduler, Metrics, Caching 等,
需要手动添加 @AutoConfigureXXX 注解,下面是启用指标收集的单元测试案例:
/** 启用指标 */
@AutoConfigureMetrics
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UserServiceTest {
// ... ...
}
使用 Bean#
如果需要在测试过程中使用 Bean,可以通过 @Autowired 注入进来:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class LivenessTest {
/** 方式一: 属性注入 */
@Autowired UserService userService;
/** 方式二: 参数注入 */
@Test
public void testForRepository(UserRepository userRepository) {
// 使用 Bean
userService.saveUser(new User(...));
// ... ...
}
}
Mock/Spy Bean#
如果需要替换 Context 中的某个 Bean,可以使用 @MockBean @SpyBean 注解。
这个 Mock 动作是临时的,仅仅对当前函数有效。
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class MyTests {
@Autowired private Reverser reverser;
@MockBean private RemoteService remoteService;
@Test
void exampleTest() {
given(this.remoteService.getValue()).willReturn("spring");
// Calls injected RemoteService
String reverse = this.reverser.getReverseValue();
assertThat(reverse).isEqualTo("gnirps");
}
}
@MockBean 会将 ApplicationContext 中的 RemoteService 替换为 mockito mock 生成的对象。
如果要基于现有对象进行mock,可以使用 @SpyBean, 从而实现最小化修改。
ReflectionTestUtils#
spring test 提供了一个强大的 ReflectionTestUtils 辅助工具。 借助 ReflectionTestUtils 可以直接修改对象中的任意字段。例如:
ReflectionTestUtils.getField(myObj, "name"). 获取myObj.nameReflectionTestUtils.setField(myObj, "name", "value"). 等价于myObj.name = "value"ReflectionTestUtils.invokeMethod(myObj, "someFunction"), 等价于myObj.someFunction()
支持对 private 字段、函数进行操作。
所以,也可以通过 ReflectionTestUtils 将某些属性替换为我们 mock 的对象:
@Test
public void testUserService(@Autowired UserService userService) {
ReflectionTestUtils.setField(userService, "userRepository", new MockUserRepository());
}
不过这个替换是永久的,用完记得恢复现场。
Context 管理#
一般来说,整个测试复用一个 ApplicationContext 就可以了。 每次启动一个新 Context 可能很耗时,但是,有需要的话,也可以通过不同的配置启动多个 Context。
// 启用 test/resources/application-one.properties
@ActiveProfiles("one")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class OneTests {
// ... ...
}
// 启用 test/resources/application-two.properties
@ActiveProfiles("two")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class TwoTests {
// ... ...
}