开课吧孤尽T31训练营学习笔记-DAY25-单元测试

334 阅读4分钟

单元测试

大家都指导单元测试很重要,但是又极少能做好单元测试,这节课主要跟着老师学习了单元测试的一些原则和最佳实践。

一、单元测试原则

1.1 宏观原则:AIR原则

宏观上,单元测试整体必须遵守 AIR 原则。

  1. A: 自动化
  2. R: 可重复性
  3. I: 独立性

1.2 实操原则:BCDE原则

  1. B: Border 边界值测试
  2. C: Correct 正确的输入,并得到预期的结果
  3. D: Design与设计文档相结合
  4. E: Error 证明程序有错

1.3 常用单元测试框架简介

image.png

二、单元测试实战

2.1 基本示例

  1. @Before每一个test case前执行
  2. @After 每一个test case后执行
@RunWith(SpringRunner.class)
@SpringBootTest
public class JUnitDemoTest {

    private static final Logger logger = LoggerFactory.getLogger(JUnitDemoTest.class);

    @BeforeClass
    public static void setUpBeforeClass() throws Exception {
        logger.debug("before class");
    }

    @AfterClass
    public static void setUpAfterClass() throws Exception {
        logger.debug("after class");
    }

    @Before
    public void setUp() {
        logger.debug("setup for this test");
    }

    @After
    public void tearDown() {
        logger.debug("tearDown for this test");
    }

    @Test
    public void testCase1() {
        logger.debug("test case 1 excute...");
    }

    @Test
    public void testCase2() {
        logger.debug("test case 1 excute...");
    }

}

运行结果

JUnitDemoTest - before class
JUnitDemoTest - Started JUnitDemoTest in 16.716 seconds (JVM running for 18.076)
JUnitDemoTest - setup for this test
JUnitDemoTest - test case 1 excute...
JUnitDemoTest - tearDown for this test
JUnitDemoTest - setup for this test
JUnitDemoTest - test case 1 excute...
JUnitDemoTest - tearDown for this test
JUnitDemoTest - after class

2.2 事务相关测试

可以控制某一个测试用例,测试完毕后,将事务回滚。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:application.properties"})
@Transactional
@Rollback
public class TransactionalTest extends AbstractTransactionalJUnit4SpringContextTests {

    private static final Logger logger = LoggerFactory.getLogger(TransactionalTest.class);

    @BeforeTransaction
    public void beforeTranscationalDo() {

    }

    @AfterTransaction
    public void afterTranscationalDo() {

    }

    // 正常,根据类注解自动回滚事务
    @Test
    public void testOne() {

    }

    // 覆盖类注解自动回滚事务, 声明为不回滚
    @Rollback(false)
    public void testTwo() {

    }

    /**
     * As of Spring 3.0,@NotTransactional is deprecated in favor of moving
     * the non-transactional test method to a separate (non-transactional)
     * test class or to a @BeforeTransaction or @AfterTransaction method. As
     * an alternative to annotating an entire class with @Transactional,
     * consider annotating individual methods with @Transactional; doing so
     * allows a mix of transactional and non-transactional methods in the
     * same test class without the need for using @NotTransactional.
     */
    @NotTransactional
    public void testThree() {

    }

}

最后的NotTransactional已经被废弃了。

推荐做法是,如果想测试一个非事务性的方法,可以将这个方法单独写到一个非事务的测试类中,或者将测试语句写在@BeforeTransaction 或者 @AfterTransaction方法中。

2.3 测试覆盖率

image.png

2.4 数据库代码相关单元测试实践


@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = AdminApplication.class)
public class MyBatisPlusTest {
    @Resource
    private UserDao userMapper;
    @Autowired
    //private DruidDataSource druidDataSource;
    private HikariDataSource dataSource;
    /**
     * 测试sql注入,
        解决方式:对参数进行特殊字符校验
     */
    @Test
    public void testLogin(){
        //sql注入例子说明
        List<User> user=userMapper.login("'' or 1 = 1 limit 1 --","123456");
        //通过以上例子可以跳过登录环节
        //验证sql注入是否跳过登录,如不能跳过,测试成功,反之失败
        //如需要检查模块是否存在sql注入漏洞可以使用mybatis 注入器AbstractSqlInjector
        QueryWrapper<User> wrapper=new QueryWrapper();
        wrapper.eq("user_name","'' or 1 = 1 limit 1 --");
        wrapper.eq("password","123456");
        List<User> user2=userMapper.selectList(wrapper);
        //如果未查询到数据通过
        assertThat(user2==null);
    }
    /**
     * 验证连接数超过的情况,获取数
     * 无连接提示超时,可以捕获异常,准备兜底方案
     */
    @Test
    public void testSqlConnectionFull(){
        List list=new ArrayList();
        try {
            //依次增加验证数据库最大承载
            for(int i=0;i<1000;i++){
                list.add(dataSource.getConnection());
            }
            List<User> users=userMapper.selectList(null);
            assertThat(users==null);
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }
    /**
     * 验证数据库连接一直占用不断增长连接的情况,避免线程死锁等情况
     */
    @Test
    public void testConnectionOverTime(){
        List<User> users=null;
        while (true){
            users=userMapper.selectList(null);
            assertThat(users==null);
        }
    }
    /**
     * 验证疯狂插入数据异常回滚
     * 思考业务场景中,长业务场景中,如果执行失败之后,依次回滚事务的问题
        一般会采用分布式事务seata 解决这一系列问题
     * 这里只是单元测试如果失败情况下,简单业务是否会回滚的问题
     */
    @Transactional(rollbackFor = Exception.class)
    @Test
    public void testTransactionRollback(){
        while (true){
            User user = new User();
            user.setUserName("2222");
            user.setPassword("123456");
            user.setRealName("testTransactionRollback");
            userMapper.insert(user);
        }
    }
    /**
     * 验证唯一约束异常、主键约束异常问题,提供兜底方案
     * 失败也需要保存数据,不能因为约束异常,插入数据失败
     * 插入失败则采用自增主键,唯一索引异常,需备份数据,便于后期数据审核
     */
    @Test
    public void testRuntimeException(){
        try{
            User user = new User();
            user.setUserName("2222");
            user.setPassword("123456");
            user.setRealName("testRuntimeException");
            assertThat(userMapper.insert(user)).isGreaterThan(0);
        }catch (Exception e){
            User user = new User();
            user.setUserName("2222");
            user.setPassword("123456");
            user.setRealName("testRuntimeException");
            userMapper.insert(user);
        }
    }

    /**
     * 采用默认mybaits-plus分页检索,检查模块分页是否正确
     * 验证每页页数过大,查询直接内存溢出等情况,需要通过mybatis-plus大数据这块增强组件
     * 提升一次查询数据的最大量
     */
    @Test
    public void testPage() {
        System.out.println("----- baseMapper 自带分页 ------");
        Page<User> page = new Page<>(1, 200000000);
        IPage<User> userIPage = userMapper.selectPage(page, new QueryWrapper<User>()
                .gt("age", 6));
        assertThat(page).isSameAs(userIPage);
        System.out.println("总条数 ------> " + userIPage.getTotal());
        System.out.println("当前页数 ------> " + userIPage.getCurrent());
        System.out.println("当前每页显示数 ------> " + userIPage.getSize());
        print(userIPage.getRecords());
        System.out.println("----- baseMapper 自带分页 ------");
    }

    @Test
    public void testSelectOne() {
        User user = userMapper.selectById(1L);
        System.out.println(user);
    }

    @Test
    public void testInsert() {
        User user = new User();
        user.setRealName("testInsert");
        user.setUserName("2222");
        user.setPassword("123456");
        assertThat(userMapper.insert(user)).isGreaterThan(0);
        // 成功直接拿会写的 ID
        assertThat(user.getId()).isNotNull();
    }

    @Test
    public void testDelete() {
        assertThat(userMapper.deleteById(3L)).isGreaterThan(0);
        assertThat(userMapper.delete(new QueryWrapper<User>()
                .lambda().eq(User::getUserName, "smile"))).isGreaterThan(0);
    }

    @Test
    public void testUpdate() {
        User user = userMapper.selectById(2);
        assertThat(user.getUserName()).isEqualTo("123");
        assertThat(user.getUserName()).isEqualTo("keep");

        userMapper.update(
                null,
                Wrappers.<User>lambdaUpdate().set(User::getPassword, "1231123").eq(User::getId, 2)
        );
        assertThat(userMapper.selectById(2).getPassword()).isEqualTo("1231123");
    }

    @Test
    public void testSelect() {
        List<User> userList = userMapper.selectList(null);
        Assert.assertEquals(5, userList.size());
        userList.forEach(System.out::println);
    }

    @Test
    public void testSelectCondition() {
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.select("max(id) as id");
        List<User> userList = userMapper.selectList(wrapper);
        userList.forEach(System.out::println);
    }


    private <T> void print(List<T> list) {
        if (!CollectionUtils.isEmpty(list)) {
            list.forEach(System.out::println);
        }
    }


}