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

参考:芋道源码