单元测试
大家都指导单元测试很重要,但是又极少能做好单元测试,这节课主要跟着老师学习了单元测试的一些原则和最佳实践。
一、单元测试原则
1.1 宏观原则:AIR原则
宏观上,单元测试整体必须遵守 AIR 原则。
- A: 自动化
- R: 可重复性
- I: 独立性
1.2 实操原则:BCDE原则
- B: Border 边界值测试
- C: Correct 正确的输入,并得到预期的结果
- D: Design与设计文档相结合
- E: Error 证明程序有错
1.3 常用单元测试框架简介
二、单元测试实战
2.1 基本示例
- @Before每一个test case前执行
- @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 测试覆盖率
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);
}
}
}