SpringbootTest
方法1:直接使用@SpringBootTest注解
直接使用@SpringBootTest注解,然后添加测试方法,直接注入需要的类,这种方式在运行测试方法时会启动spring容器,数据库等采用项目的默认配置,如果项目过大,测试会很慢。
方法2:按需加载
只加载测试需要的类,采用H2数据库,不影响项目数据库的数据。运行速度快。
1.依赖
<!-- Test 测试相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId> <!-- 单元测试,我们采用 H2 作为数据库 -->
<artifactId>h2</artifactId>
<!-- <scope>test</scope>-->
</dependency>
<dependency>
<groupId>com.github.fppt</groupId> <!-- 单元测试,我们采用内嵌的 Redis 数据库 -->
<artifactId>jedis-mock</artifactId>
<version>1.0.6</version>
</dependency>
<dependency>
<groupId>uk.co.jemos.podam</groupId> <!-- 单元测试,随机生成 POJO 类 -->
<artifactId>podam</artifactId>
<version>7.2.11.RELEASE</version>
</dependency>
2.测试注解
//默认不加载web环境,指定测试启动类为一个空的bean,这样就不会启动spring容器了
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseApplication.Application.class)
//指定测试环境的配置文件
@ActiveProfiles("unit-test")
// 每个单元测试结束后,清理 DB
@Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
//测试模块中引入mapper
@MapperScan("com.lzp.springbootnew.test.mapper")
//我们自己的测试类继承这个类即可
public class BaseApplication {
//引入需要的自动配置 datasource mybatis
@Import({
// DB 配置类
DataSourceAutoConfiguration.class, // Spring DB 自动配置类
DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类
//测试开始前需要创建项目对应的表,这是一个自定义的配置类
SqlInitializationTestConfiguration.class, // SQL 初始化
// MyBatis 配置类
MybatisAutoConfiguration.class, // MyBatis 的自动配置类
// Redis 配置类
RedisAutoConfiguration.class, // Spring Redis 自动配置类
})
public static class Application {
}
}
//SqlInitializationTestConfiguration类,这个类一定要有,初始化建表sql用的
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(AbstractScriptDatabaseInitializer.class)
@ConditionalOnSingleCandidate(DataSource.class)
@ConditionalOnClass(name = "org.springframework.jdbc.datasource.init.DatabasePopulator")
@Lazy(value = false) // 禁止延迟加载
@EnableConfigurationProperties(SqlInitializationProperties.class)
public class SqlInitializationTestConfiguration {
@Bean
public DataSourceScriptDatabaseInitializer dataSourceScriptDatabaseInitializer(DataSource dataSource,
SqlInitializationProperties initializationProperties) {
DatabaseInitializationSettings settings = createFrom(initializationProperties);
return new DataSourceScriptDatabaseInitializer(dataSource, settings);
}
static DatabaseInitializationSettings createFrom(SqlInitializationProperties properties) {
DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setSchemaLocations(properties.getSchemaLocations());
settings.setDataLocations(properties.getDataLocations());
settings.setContinueOnError(properties.isContinueOnError());
settings.setSeparator(properties.getSeparator());
settings.setEncoding(properties.getEncoding());
settings.setMode(properties.getMode());
return settings;
}
}
3.测试配置文件
spring:
main:
# lazy-initialization: true # 开启懒加载,加快速度
banner-mode: off # 单元测试,禁用 Banner
--- #################### 数据库相关配置 H2内嵌数据库 ####################
spring:
datasource:
url: jdbc:h2:mem:mybatis;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value;
# url: jdbc:h2:D:/h2/testdb;MODE=MYSQL
hikari:
driver-class-name: org.h2.Driver
username: root
password: 123456
#初始化sql 建表
sql:
init:
schema-locations: classpath:/sql/create_tables.sql
4.测试的logback.xml
<configuration>
<!-- 引用 Spring Boot 的 logback 基础配置 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
</configuration>
5.测试类
//这里service实现类要自己引入,因为spring容器没启动
@Import(UserService.class)
//继承标题二中的类
class UserServiceTest extends BaseApplication{
//注入service实现类
@Resource
UserService userService;
//service中引入的mapper也要注入 这里可以注入是因为用了mapperScan注解
@Resource
UserMapper userMapper;
//创建测试方法
@Test
public void test01(){
UserDO userDO = new UserDO();
userDO.setName("zhangsan");
userDO.setAge(12);
//查询前插入
userService.insert(userDO);
//测试查询
String userName = userService.getUserName(1);
System.out.println(1);
}
}
6.测试目录结构
7.测试语法
待测类
测试 uerService的insert方法
@Test
void testInsert() {
//1.生成随机UserDO对象
UserDO userDO = RandomUtils.randomPojo(UserDO.class,o->{
//这里某个字段有数据范围时需要单独指定 比如性别之类的
o.setAge(randomInteger());
o.setName(randomString());
});
// mock homeService 的方法 当调用homeService.getHomeList()时 返回 mock出的home对象
Home home = randomPojo(Home.class);
//这里的作用是当userService的insert方法或者getUserName方法用到了homeService的getHomeList方法,
//例如user实体类中有home这个属性 新增时需要根据homeId查询出home对象,填充到user对象中
//这里可以模拟homeService的getHomeList方法返回值,就是上面生成的随机对象,
//如果不这样做当使用到homeService的getHomeList方法时,会报空指针异常,因为数据库中没有home的数据
when(homeService.getHomeList()).thenReturn(home);
//测试新增方法
Long id = userService.insert(userDO);
//为了验证新增成功,需要把刚新增的查出来,和新增时传入的对象作比较,相等即为成功
UserDO userSelect = userService.getUserName(id);
//断言判断两个对象是否相等,不相等则测试不通过
assertPojoEquals(userDO, userSelect);
}
randomPojo方法
@SafeVarargs
public static <T> T randomPojo(Class<T> clazz, Consumer<T>... consumers) {
//生成随机对象
T pojo = PODAM_FACTORY.manufacturePojo(clazz);
// 非空时,回调逻辑。通过它,可以实现 Pojo 的进一步处理,重新设置那种有数据范围的属性
if (ArrayUtil.isNotEmpty(consumers)) {
Arrays.stream(consumers).forEach(consumer -> consumer.accept(pojo));
}
return pojo;
}
assertPojoEquals方法
/**
* 比对两个对象的属性是否一致
*
* 注意,如果 expected 存在的属性,actual 不存在的时候,会进行忽略
*
* @param expected 期望对象
* @param actual 实际对象
* @param ignoreFields 忽略的属性数组
*/
public static void assertPojoEquals(Object expected, Object actual, String... ignoreFields) {
//获取期望对象的所有字段
Field[] expectedFields = ReflectUtil.getFields(expected.getClass());
//遍历期望对象的所有字段,也就是只判断期望对象中有的并且目标对象也有的字段
Arrays.stream(expectedFields).forEach(expectedField -> {
// 忽略 jacoco 自动生成的 $jacocoData 属性的情况
if (expectedField.isSynthetic()) {
return;
}
// 如果是忽略的属性,则不进行比对
if (ArrayUtil.contains(ignoreFields, expectedField.getName())) {
return;
}
// 忽略不存在的属性 期望对象有 传入的对象没有
Field actualField = ReflectUtil.getField(actual.getClass(), expectedField.getName());
if (actualField == null) {
return;
}
// 比对 一个属性一个属性对比
Assertions.assertEquals(
ReflectUtil.getFieldValue(expected, expectedField),
ReflectUtil.getFieldValue(actual, actualField),
String.format("Field(%s) 不匹配", expectedField.getName())
);
});
}
测试异常
@Test
public void testCreatUser_max() {
// 准备参数
UserCreateReqVO reqVO = randomPojo(UserCreateReqVO.class);
// mock 租户账户额度不足
TenantDO tenant = randomPojo(TenantDO.class, o -> o.setAccountCount(-1));
//doNothing()用于测试无返回值的方法,无返回值也就没有thenreturn了
//作用就是userService.createUser(reqVO)方法执行时,检查租户额度时会检测到租户额度不足,
doNothing().when(tenantService).handleTenantInfo(argThat(handler -> {
handler.handle(tenant);
return true;
}));
// 调用,并断言异常
//ErrorCode USER_COUNT_MAX = new ErrorCode(1002003008, "创建用户失败,原因:超过租户最大租户配额({})!");
assertServiceException(() -> userService.createUser(reqVO), USER_COUNT_MAX, -1);
}
/**
* 执行方法,校验抛出的 Service 是否符合条件
*
* @param executable 业务异常,也就是要抛出异常的方法,传入lambada表达式
* @param errorCode 错误码对象 异常时的错误码和提示信息
* @param messageParams 消息参数 抛出异常时的异常值
*/
public static void assertServiceException(Executable executable, ErrorCode errorCode, Object... messageParams) {
// 调用方法 获得方法抛出的异常,这里如果没有异常会报错
ServiceException serviceException = assertThrows(ServiceException.class, executable);
// 校验错误码
Assertions.assertEquals(errorCode.getCode(), serviceException.getCode(), "错误码不匹配");
//这里需要进行格式化,是因为构建ServiceException时,对message进行了格式化
String message = ServiceExceptionUtil.doFormat(errorCode.getCode(), errorCode.getMsg(), messageParams);
//判断提示信息是否一致,不一致抛出异常
Assertions.assertEquals(message, serviceException.getMessage(), "错误提示不匹配");
}
原service方法
最终的message
8.使用内嵌redis
参考:芋道源码