Fit 提供了一个简单高效的测试框架 fit-test-framework,用于在开发时进行测试。它遵循 IoC 原则降低代码耦合度,将待测试类依赖的组件以 Bean 对象的形式注入到容器里,实现业务代码与测试代码的解耦。本节介绍测试框架的功能和实现,以及运行流程。
fit-test-framework 提供了 @FitTestWithJunit 注解支持Junit4和Junit5 使用,绕过业务代码里的 main 方法,将待测试类数组注入到容器中。在这一步中,容器会通过待测试类中添加的注解 @ScanPackages 扫描指定路径下的 Bean 对象,方便容器创建待测试类 Bean 对象时注入。
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(FitExtension.class)
public @interface FitTestWithJunit {
/**
* 需要注入到容器的组件类型数组。
*
* @return 表示需要注入到容器的组件类型数组的 {@link Class}{@code <?>[]}。
*/
Class<?>[] classes() default {};
}同时,它还提供 @Mocked 注解用于创建以及注入模拟实例,运行单元测试时不需要访问持久化数据,实现测试代码与业务基础设施的解耦。
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Fit
public @interface Mocked {}fit-test-framework 分别对 Junit4 和 Junit5 提供扩展类 FitRunner 和 FitExtension,分别与 Junit4 的注解 RunWith 和 Junit5 的注解 ExtendWith 搭配使用,以便在测试开始的时候根据类型自动创建测试框架的管理类。
/**
* Junit4 的自定义扩展类。
*/
public class FitRunner extends BlockJUnit4ClassRunner {
FitTestManager manager;
public FitRunner(Class<?> clazz) throws InitializationError {
super(clazz);
this.manager = FitTestManager.create(clazz);
}
@Override
protected Object createTest() throws Exception {
Object testInstance = super.createTest();
this.manager.prepareTestInstance(testInstance);
return testInstance;
}
}
/**
* Junit5 的自定义扩展类。
*/
public class FitExtension implements BeforeAllCallback, TestInstancePostProcessor {
private FitTestManager manager;
@Override
public void beforeAll(ExtensionContext context) {
this.manager = FitTestManager.create(context.getRequiredTestClass());
}
@Override
public void postProcessTestInstance(Object testInstance, ExtensionContext context) {
this.manager.prepareTestInstance(testInstance);
}
}TestContext 为测试框架提供上下文,依赖测试框架使用的插件类 TestPlugin 和测试框架使用的运行时 TestFitRuntime。上下文中还会获取单测类的类对象 testClass 和单测试类的实例对象 testInstance。
public class TestContext {
private final Class<?> testClass;
private final TestPlugin testPlugin;
private Object testInstance;
}运行测试用例时,程序先通过 Junit4 的 RunWith 注解(如果是 Junit5 则通过 ExtendWith 注解)注入 Fit 测试框架扩展类,启动测试框架。随后通过注解注入待测试的类。这里待测试类中的 ScanPackages 指定扫包路径,程序会将路径下的所有Bean对象加载到容器中,在测试用例中将对应的Bean 对象注入到依赖中实例化对象,参考下图。这里所有对象实例化完成,开始跑测试用例。
本节介绍 mybatis 数据库测试框架的实现。在业务场景中,海量数据存储到业务数据库。在测试时,我们往往需要切换业务数据库到测试数据库,以确保业务数据不受影响,独立测试开发人员编写的业务代码。我们的 mybatis 测试框架实现了数据库的切换,业务代码连接 PostgreSQL,测试代码连接 Java 开发的嵌入式(内存级别) H2 数据库,它具有启动速度快,而且可以关闭持久化功能,每一个用例执行完随即还原到初始状态等优点。
FIT 框架提供了@MybatisTest注解来进行数据库的测试,其中,DatabaseModel可指定为MYSQL或POSTGRESQL:
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@FitTestWithJunit
@EnableMybatis
@EnableDataSource
public @interface MybatisTest {
/**
* 需要注入到容器的组件类型数组。
*
* @return 表示需要注入到容器的组件类型数组的 {@link Class}{@code <?>[]}。
*/
@Forward(annotation = FitTestWithJunit.class, property = "includeClasses") Class<?>[] classes() default {};
/**
* 获取测试数据源兼容模式。
*
* @return 表示数据源兼容模式的 {@link DatabaseModel}。
* @see EnableDataSource#model()
*/
@Forward(annotation = EnableDataSource.class, property = "model") DatabaseModel model() default DatabaseModel.NONE;
}以下是一个mybatis测试用例,其中,数据源兼容模式选择了POSTGRESQL
@MybatisTest(classes = {PeopleRepo.class}, model = DatabaseModel.POSTGRESQL)
@Sql(scripts = "sql/test_create_table.sql")
@DisplayName("测试 PeopleMapper")
public class PeopleRepoTest {
@Fit
private PeopleRepo mapper;
@Test
@DisplayName("插入数据成功")
void shouldOkWhenInsert() {
this.mapper.insert("Student", 28, "Xiao Ming");
List<String> students = mapper.select("Student", 28);
assertThat(students.size()).isEqualTo(1);
assertThat(students.get(0)).isEqualTo("Xiao Ming");
}
}FIT 在提供方便易用的MVC框架的同时,也提供了相应的测试工具。
FIT 提供了@MvcTest注解将待测试类数组注入到测试代码中。
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@FitTestWithJunit
@EnableMockMvc
public @interface MvcTest {
/**
* 需要注入到容器的组件类型数组。
*
* @return 表示需要注入到容器的组件类型数组的 {@link Class}{@code <?>[]}。
*/
@Forward(annotation = FitTestWithJunit.class, property = "includeClasses") Class<?>[] classes() default {};
}示例如下:
@MvcTest(classes = {TestController.class, Config.class})
public class MvcDemoTest {
...
}本测试框架为客户端提供了HTTP请求的构建器MockMvcRequestBuilder和MockRequestBuilder,它们提供了一系列方法,允许开发者灵活地定义请求的细节,如请求方法、URL、请求头、请求参数和请求体等。
MockMvcRequestBuilder为模拟 MVC 客户端提供请求的构建集合,它提供了一系列静态方法构建 HTTP 请求方法,并返回一个新创建的MockRequestBuilder:
| 方法 | 解释 |
|---|---|
| get(String url) | 构建一个 GET 请求 |
| post(String url) | 构建一个 POST 请求 |
| put(String url) | 构建一个 PUT 请求 |
| patch(String url) | 构建一个 PATCH 请求 |
| delete(String url) | 构建一个 DELETE 请求 |
MockRequestBuilder提供了一系列方法用于设置请求的各项参数及内容:
| 方法 | 解释 |
|---|---|
| param(String name, String value) | 为请求插件结构体添加键值对参数 |
| param(String name, List values) | 为请求插件结构体添加键值对参数 |
| responseType(Type responseType) | 设置客户端请求结果的类型 |
| port(int port) | 设置客户端请求的端口号 |
| header(String name, String header) | 设置请求结构体的消息头 |
| header(String name, List headers) | 设置请求结构体的消息头 |
| entity(Entity entity) | 设置请求结构体的消息体内容 |
| formEntity(MultiValueMap<String, String> formEntity) | 设置 Http 请求的 Form 格式的消息体内容 |
| jsonEntity(Object jsonObject) | 设置 Http 请求的 Json 格式的消息体内容 |
| client(HttpClassicClient httpClassicClient) | 设置 Http 客户端 |
| build() | 构建客户端的请求参数 |
使用示例如下:
//构建POST请求
MockRequestBuilder requestBuilder = MockMvcRequestBuilders.post("/user").jsonEntity(userDto).responseType(Void.class);//构建GET请求并添加参数键值对
MockRequestBuilder requestBuilder = MockMvcRequestBuilders.get("/user")
.param("name", "zhangsan")
.param("email", "zhangsan@test.com")
.responseType(TypeUtils.parameterized(UserVo.class, new Type[] {UserEntity.class}));FIT 框架提供了控制器的测试类 MockMvc,它允许开发者在不启动服务器的情况下模拟 HTTP 请求和响应,验证控制器的行为,它提供了perform()函数用以执行MockRequestBuilder模拟的HTTP请求,同时提供了streamPerform()函数以获取流式数据。使用示例如下:
public class UserControllerTest {
@Fit
private MockMvc mockMvc;
private HttpClassicClientResponse<?> response;
@Test
void shouldReturnOkWhenGetUser() {
...
MockRequestBuilder requestBuilder = MockMvcRequestBuilders.get("/user")
.param("name", "zhangsan")
.param("email", "zhangsan@test.com")
.responseType(TypeUtils.parameterized(UserVo.class, new Type[] {UserEntity.class}));
this.response = this.mockMvc.perform(requestBuilder);
assertThat(this.response.statusCode()).isEqualTo(200);
}
}其中,MockMvc实例可直接通过依赖注入的方式获取。
