单元测试规范&Mybatis+SpringBoot+H2实战

背景介绍

为了长期持续高质量、高效率的迭代,必须遵守一定的研发规范,其中主要包括静态代码的扫描和单元测试两个部分。

风险认知

行业的普遍共识,风险识别暴露的越晚,修复的成本越高。
在这里插入图片描述
好代码不是一蹴而就的,是持续重构出来的
在这里插入图片描述

目标

在这里插入图片描述

技术选型

在这里插入图片描述

最佳实践
安装sonarLint

在这里插入图片描述
本地IntelliJ IDEA安装sonarlint,根据提示重启IntelliJ IDEA即可
有两个选项:根据本地修改扫描和全量扫描。可以根据实际情况开启。扫描结果如下:
在这里插入图片描述
在这里插入图片描述

此处扫描出范型约束丢失的可能运行时异常,风险等级为Major。Blocker级别为必须修复,否则打包无法通过。

可以通过右键展开全部:在这里插入图片描述

在代码编写过程中,也可同步识别风险:在这里插入图片描述
在Sonar中展示的代码规则违反分类数量统计,分为5个级别:Blocker、Critical、Major、Minor、Info。其中Blocker、Critical是需要重点避免的。

负责人&核心链路
应用负责人
应用 负责人(待定)

质量标准
应用 代码质量要求/核心链路情况

单测
单元测试是保证持续迭代的重要手段,下面提供应用负责人,与质量标准。

在这里插入图片描述
几个要点:

不要为了单测而单测,单测可以发现代码结构的不合理处,反向推动代码优化。
单测需要快速、可重复、不依赖外部数据接口(外部数据不稳定)。
不要滥用mock,外部调用(包括数据库)尽量用个Facade封装,只需要 mock Facade即可。
sqlmap的验证需要启动容器和数据库连接,需要单独测试(后面会写到)。

实战
业务单元测试

我们使用powermock + junit进行mock ,关于使用可以参考我之前写过的一篇文章 点我查看使用,为了避免不可预知的兼容性问题,建议maven版本如下

    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-core</artifactId>
		<version>3.3.3</version>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.powermock</groupId>
        <artifactId>powermock-api-mockito2</artifactId>
        <version>1.7.3</version>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.powermock</groupId>
        <artifactId>powermock-module-junit4</artifactId>
        <version>1.7.3</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
		<version>4.13.1</version>
        <scope>test</scope>
    </dependency>

我们来测试一个接口
UserController#add:
其主要的流程图
在这里插入图片描述

单测的结构
在这里插入图片描述

@RunWith(PowerMockRunner.class)
@PrepareForTest({RequestContextHolder.class}) // 需要 mock 类静态方法,需要在此声明
public class BaseUnitTest {

    @Before
    public void init() {
        System.out.println("start run unit test");
    }

    /**
     * API rpc mock
     */
    @Mock // 声明这是 mock bean
    private BackendUserApi backendUserApi;

    @Mock
    private RedisService redisService;

    @Mock
    private BCryptPasswordEncoder passwordEncoder;

    @Before
    public void before() {
        /*
        mock rpc result
         */
        BaseResult<Integer> baseResult = new BaseResult<>();
        baseResult.setCode(BaseResult.SUCCESS);
        baseResult.setData(1);
        PowerMockito.when(backendUserApi.addBackendUser(notNull(), notNull())).thenReturn(baseResult);

        /*
        mock static method http request
         */
        PowerMockito.mockStatic(RequestContextHolder.class);

        MockHttpServletRequest mockHttpServletRequest = new MockHttpServletRequest();
        mockHttpServletRequest.setParameter(AuthConstants.AUTH_HEADER, "test");
        ServletRequestAttributes requestAttributes = new ServletRequestAttributes(mockHttpServletRequest);
        PowerMockito.when(RequestContextHolder.getRequestAttributes()).thenReturn(requestAttributes);

        /*
        mock user info
         */
        PowerMockito.when(redisService.getCacheObject(anyString())).thenReturn(DataBuilder.buildUserCacheJson("unitTest"));

        /*
        mock encode
         */
        PowerMockito.when(passwordEncoder.encode(notNull())).thenReturn("encoded");
    }


    /**
     * 测试数据的组装
     */
    public static class DataBuilder {

        public static AddUserDTO buildUserDTO(String name,
                                       String phone,
                                       Long enrollmentDate,
                                       String email,
                                       String sex,
                                       Long birthday,
                                       String freezeStatus,
                                       String remark,
                                       List<Long> orgIds) {
            AddUserDTO addUserDTO = new AddUserDTO();
            addUserDTO.setName(name);
            addUserDTO.setPhone(phone);
            addUserDTO.setEnrollmentDate(enrollmentDate);
            addUserDTO.setEmail(email);
            addUserDTO.setSex(sex);
            addUserDTO.setBirthday(birthday);
            addUserDTO.setFreezeStatus(freezeStatus);
            addUserDTO.setRemark(remark);
            addUserDTO.setOrgIds(orgIds);
            return addUserDTO;
        }

        /**
         * 快速组装测试数据
         * @param name
         * @return
         */
        public static AddUserDTO buildUserDTO(String name) {
            // 调用重载
            return buildUserDTO(name, "13011111111", 11111111111L, "test@mail.com", "1", 111L, "1", "remark", Lists.newArrayList(1L));
        }

        /**
         * mock redis cache user info
         * @param name
         * @return
         */
        public static String buildUserCacheJson(String name) {
            com.alibaba.fastjson.JSONObject jsonObject = new com.alibaba.fastjson.JSONObject().fluentPut("userId", 1)
                    .fluentPut("userName", name)
                    .fluentPut("token", "token")
                    .fluentPut("orgIds", Lists.newArrayList(1))
                    .fluentPut("roleIds", Lists.newArrayList(1));
            return jsonObject.toString();
        }
    }

}
public class UserControllerTest extends BaseUnitTest {

    @Spy // 为一个真实的实例
    @InjectMocks  // 依赖的bean如果是mock注入mock实例,如果是真实bean,注入真实bean
    private BackendUserController backendUserController = new BackendUserController();

    @Spy
    @InjectMocks
    private BackendUserService backendUserService = new BackendUserServiceImpl();

    @Spy
    @InjectMocks
    private BackendUserManager backendUserManager = new BackendUserManager();

    /**
     * 这里是test case,assert 结果
     */
    @Test
    public void testQueryByUserId() {
        BaseResult<Integer> unitTest = backendUserController.add(DataBuilder.buildUserDTO("unitTest"));
        Assert.assertTrue(unitTest.isSuccess());
    }
}

运行junit测试

在这里插入图片描述

可以点击idea覆盖率统计,即可输出单测的覆盖率情况,也能高亮代码覆盖情况。
在这里插入图片描述

sqlmap的测试

上面的测试是真正的单元测试,没有启动spring容器(因为耗时并且没必要),很多时候我们写完sqlmap需要去测试,这个时候需要启动容器加载最小的数据源组件,下面是实践参考。
在这里插入图片描述

配置方式如下:

@Configuration
@ActiveProfiles("test")
public class MybatisConfiguration {

    /**
     * datasource
     */
    @Bean
    public DataSource dataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUrl("xxxxxx");
        dataSource.setUsername("xxx");
        dataSource.setPassword("xxxx");
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");

        dataSource.setMinIdle(0);
        dataSource.setInitialSize(0);
        dataSource.setQueryTimeout(3);
        dataSource.setMaxWait(3000);
        dataSource.setPhyTimeoutMillis(3000);
        dataSource.setMaxActive(2);
        return dataSource;
    }


    /**这里配置SqlSessionFactory */
    @Bean
    public SqlSessionFactory sqlSessionFactoryBean(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        /**
         * 这里值得注意的是我的一个Mapper接口对应了两个sqlmap的文件,这里试用通配符的方式加载。
         * 如果不确定自己的通配符是否正确,可以动态debug到此处动态调用解析结果看是否加载到Resource[]
         */
        ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
        Resource[] resources = resourcePatternResolver.getResources("classpath*:**/*Mapper.xml");
        sqlSessionFactoryBean.setMapperLocations(resources);

        return sqlSessionFactoryBean.getObject();
    }


    @Bean
    public DataSourceTransactionManager dataSourceTransactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer(SqlSessionFactory sqlSessionFactory) {
        MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();

        mapperScannerConfigurer.setBasePackage("com.xxxx.mappaer");
        mapperScannerConfigurer.setSqlSessionFactory(sqlSessionFactory);
        return mapperScannerConfigurer;
    }


	/*
	* 提供一个直接执行SQL的方式
	*/
    @Bean
    @Autowired
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }

}
Mapper的测试
@Slf4j
@Transactional
@Rollback // 测试完毕自动回滚测试数据
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = MybatisConfiguration.class)
public class UserMapperTest {

    @Autowired
    private UserMappaer userMappaer;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Test
    public void testQueryByCode() {
		// 测试写入
        jdbcTemplate.execute("INSERT INTO user(id, name) VALUES(1, 'micro')");
        UserDO userDO = userMappaer.queryByCode("micro");
		// 测试断言mapper查询的正确性
        Assert.assertTrue(userDO != null && userDO.getName().equals("micro"));
    }
}
h2内存数据库

上面的方案DAO的测试需要连接远程DB,这里存在的风险是测试环境数据的不稳定问题与数据的初始化问题,同时无法做到真正的“本地测试”。

可以引入H2数据库,其可以在内存构建数据库实例并完成单测运行。

下面提供使用方式

需要的maven依赖:

    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.4</version>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <version>1.4.197</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <version>2.4.5</version>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13.1</version>
        <scope>test</scope>
    </dependency>

application.yml

spring:
  datasource:
    driver-class-name: org.h2.Driver
    schema: classpath:schema.sql #,每次启动程序,程序都会运行schema.sql文件,对数据库的数据操作
    data: classpath:data.sql #,每次启动程序,程序都会运行data.sql文件,对数据库的数据操作
    url: jdbc:h2:mem:db_users;MODE=MYSQL #配置h2数据库的连接地址
    username: sa
    password:
  h2:
    console:
      enabled: true #开启web console功能

mybatis:
  mapper-locations: classpath*:mappers/**/*.xml
  type-aliases-package: xxx.com.entity

表结构定义DDL schema.sql

CREATE TABLE `user`
(
  `id` bigint NOT NULL,
  `name` varchar(20)
   primary key(`id`)
);

初始化数据data.sql

INSERT INTO `user` (`id`, `name`) VALUES
  (0, 'micro'),
  (1, 'micro1'),
  (2, 'micro2');

单测编写

@SpringBootTest(classes = {DataSourceAutoConfiguration.class, MybatisAutoConfiguration.class, MybatisConfiguration.class})
@RunWith(SpringRunner.class)
public class UserMapperTest {

    @Autowired
    private UserMapper userMapper;

    /**
     * test case
     */
    @Test
    public void testQueryByCode() {
        UserDO userDO = userMapper.getByName("micro");
        Assert.assertTrue(userDO != null && userDO.getName().equals("micro"));
    }
}
总结

单元测试的威力是在持续重构中,重构是改变既有代码设计的必要手段。
不要盲目自信自己的代码, QA资源是宝贵的,开发自测是大的趋势。
评审需求的时间应该包括自测时间。

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值