赞
踩
方法1:直接使用@SpringBootTest注解
直接使用@SpringBootTest注解,然后添加测试方法,直接注入需要的类,这种方式在运行测试方法时会启动spring容器,数据库等采用项目的默认配置,如果项目过大,测试会很慢。
方法2:按需加载
只加载测试需要的类,采用H2数据库,不影响项目数据库的数据。运行速度快。
<!-- 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>
//默认不加载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; } }
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
<configuration>
<!-- 引用 Spring Boot 的 logback 基础配置 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
</configuration>
//这里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); } }
待测类
测试 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
参考:芋道源码
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。