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

语言: CN / TW / HK

单元测试

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

一、单元测试原则

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后执行

``` java @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 事务相关测试

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

``` java @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 数据库代码相关单元测试实践

``` java

@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=userMapper.login("'' or 1 = 1 limit 1 --","123456"); //通过以上例子可以跳过登录环节 //验证sql注入是否跳过登录,如不能跳过,测试成功,反之失败 //如需要检查模块是否存在sql注入漏洞可以使用mybatis 注入器AbstractSqlInjector QueryWrapper wrapper=new QueryWrapper(); wrapper.eq("user_name","'' or 1 = 1 limit 1 --"); wrapper.eq("password","123456"); List 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 users=userMapper.selectList(null); assertThat(users==null); } catch (SQLException throwables) { throwables.printStackTrace(); } } / * 验证数据库连接一直占用不断增长连接的情况,避免线程死锁等情况 / @Test public void testConnectionOverTime(){ List 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);
    }
}

} ```