createfile 失败:代码3_利用 Junt 维护代码质量

1b0a872f96faaf95379d5465a9e88ebf.gif

86d733b269c3d5ba8d80627c43b51b32.png

一、写本文背景

说到 Junit,很多人都知道非常强大的代码逻辑检测框架,但在平时项目中,我发现两个问题:

  1. 开发人员并不喜欢写UT,其中有包括很多资深工程师和架构师等;

  2. 写UT仅是为了验证即时代码的正确性,因此让 UT 变成了一次性的,且只为了本次代码的覆盖而写;

二、简单栗子热热身

假设我们要测试个除法运算,如div(a,b) 那么就要针对c=a/b做分别的假设和预期结果

public class MathService {
/**
* c = a/b
*/
public int div(int a, int b) {
return a / b;
}
}
  1. 假设a=10,b=5,c应为2(ab正常情况)

  2. 假设a=10,b=0,应该会抛出异常 (除数为0情况)

  3. 假设a=0,b=10,结果应是0 (被除数为0情况)

  4. 假设a=17(质数),b=8,那么是2(被除数为质数情况,主要是验证不能整除的情况)…(当然还有其它的假设和预期结果)

@Test
public void addIfAandBareNormal() {
int c = mathService.div(10, 5);
Assert.assertEquals("验证div失败", 2, c);
}
//预期为异常的写法注意一下,最好写精确是什么异常,不要直接写Exception
@Test(expected = ArithmeticException.class)
public void addIfBIsZero() {
mathService.div(10, 0);
}

@Test
public void addIfAIsZero() {
int c = mathService.div(0, 10);
Assert.assertEquals("验证div失败", 0, c);
}

@Test
public void addIfAisPrime() {
int c = mathService.div(17, 8);
Assert.assertEquals("验证div失败", 2, c);
}

执行效果图:所有方法都是成功的

a7e40b749c0601621cb241cedf2accf3.png

针对测试类或方法覆盖率

5fda3eaf2dd64a8f3901230bd48c3120.png

三、工程师并不喜欢写UT的原因

咋一看上边这个简单的除法,UT比本身的代码多了几倍,这里也是为了证明写UT的工作确实并不是件容易的工作,相反反而有点费劲,因此多数的开发并不喜欢写UT,虽然也知道UT的重要性和功能强大。但个人经验来说,这么多年工作的几个公司中,几乎没有工程师愿意写UT,更不用说喜欢了,还有很多工程师没有写过,甚至是资深工程师,加之在平时的业务代码中逻辑的复杂性,各种外部环境,多方依赖等各种情况更让人不知怎么写UT。

四、写UT的几个难点

1.多种输入条件组合导致要写的case比较多,甚至比本身的代码要多得多,且针对多次变更的复杂度极高的老代码更让人望而却步; 二八原则用在这里极为恰当,正常逻辑可能只有1个case,异常逻辑要写5,6个case; 几乎没有看到写得好的业务代码,因为业务变更频繁和快速上线导致多次变更以后兼容代码会很多,甚至最后惨不忍睹; 2.依赖的外部条件导致Case不好写

a.依赖数据库,执行一次以后,第二次结果就不一样了,比如我测试一个save,update或delete等;

b.与多方联调,很多地方根本没有测试环境,只有生产环境,且根本没办法直接访问的,如与支付宝对接支付接口,涉及到下单,支付,回调等流程的UT,按正常流程根本无法写; 3.针对业务逻辑的异常处理等的代码覆盖很困难 有时写UT时发现有些代码是永远不可能覆盖到废代码,有些代码也根本不会抛出接口中声明的异常等

如以下这段,有些异常,我们正常去写CASE,这简直没办法通过输入来产生这些预期的异常,且有些异常永远不会抛出,如HttpURLConnection,不可能拔网线关网络来实现吧:)

try{
httpClient.get("http://xxxx/getUser");
//...
}catch (NoSuchMethodException e){
//...
}catch (IllegalAccessException e){
//...
}catch (HttpURLConnection e){
//...
}catch (IOException e){
//...
}

4.UT可用于代码回归验证,因此UT也需要维护假设有一个业务突然变更,那原来代码逻辑更新,写好的UT回归测试必然过不了,那么UT也需要更变,因此 UT也需要跟着代码一起维护,维护成本也比较高;

五、如何真正的使用UT达到我们的要求

说了这么多UT的难点,相信我们已知道写UT固然不是信手拈来的活,但为什么还要写,能为我们带来什么好处吗?答案是肯定的;

先说一个自身的案例,当年在一互联网创业公司,刚好本人担任基础架构师在架构组一同推UT,开始我也比较排斥,毕竟已经很忙了,还要花时间UT,但多次讨论和分析下来决定试一试,然后定了几个有几个是强制的要求,

上线分支必须达到:

  1. 所有UT方法100%成功

  2. UT的代码覆盖率>=80%

自从保证了这两个点,我们组的bug几乎没有了,而且功能性bug几乎一个都没有,因此确实是奇效,后来整个公司要求,当然业务组相应要求低一点,但核心链路必须写UT,效果确实非常明显。

我从中得到经验是什么呢?

1.开始很痛苦,但熟能生巧

也许开始写UT感觉到痛苦,费时,但在写UT习惯之后,我们写代码时就会自然考虑到很多Case,因此代码的复杂度我们会非常注意;

  1. 非常注意代码的规范性和可读性,几乎不可能再写嵌套复杂的代码,还会使用工具来检查,如sonar,阿里p3c等,对代码的规范和复杂度都非常有指导和约束作用;

  2. 每个类和方法都不会太长,且非常注意重用性,反过来说,重用的代码UT不用写,且促进我们去抽象,去改善代码结构和质量。

2.能提升重构水平
当代码到达一定的覆盖率时,覆盖不到或很难覆盖到的代码会强制我们重构,因此可以大大改善代码结构;

这点特别针对try…然后后边一堆catch的代码改善非常明显;

如上边的try…后边的一堆catch,一般业务逻辑的代码针对这么多的异常也不可能一一处理,其实很多异常是可以合并处理的,如果不需要特殊处理的异常,可以统一起这些异常;
try{
httpClient.get("http://xxxx/getUser");
//...
}catch (NoSuchMethodException|IllegalAccessException|HttpURLConnection|IOException e){
//...
}
3.多使用Mock测试减少状态规避外部依赖

针对外部环境的依赖,正常流程肯定是没办法测试的,但现在有针对UT的Mock框架,如与Junit结合使用的Powermock,可为我们排除外界干扰,db数据变了或联调的外界环境问题等都完全不是问题

假设我有一个登录功能LoginService,需要调用UserService来取用户,假设UserService是soa或访问了db,但我们并不关心UserService的逻辑是什么,我们只要验证LoginServiceImpl的正确性;

Mock版Demo代码如下:

@Service
public class LoginServiceImpl implements ILoginService {

@Autowired
private IUserService userService;

public boolean login(String name, String passwrd) {
UserDto userDto = userService.getUserByName(name);
if (userDto.getName().endsWith(name) && userDto.getPassword().equals(passwrd)) {
log.info("{}登录成功",userDto.getName());
return true;
}
log.info("{}登录失败,用户名或密码错",userDto.getName());
return false;
}
}

我们明确我们要测试的目标是LoginServiceImpl,因此我们要将依赖的外部接口Mock掉

public class LoginServiceImplMockTest{

/**
* 当前要测试的类,使用InjectMocks注解
*/
@InjectMocks
private LoginServiceImpl loginService = new LoginServiceImpl();

/**
* 由于LoginServiceImpl中使用了IUserService,假设IUserService是一个soa接口,本地可能没办法测试
* 我们只想测试LoginServiceImpl,因此把IUserService mock掉
*/
@Mock
private IUserService userService;

@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
}

/**
* 1. 造mock数据
* 2. 设置要mock的接口
Mockito.when(userService.getUserByName(Mockito.anyString())).thenReturn(userDto);
* 3. 调用本次要UT的接口:登录
* 4. 使用Assert:验证登录结果
*/
@Test
public void loginSuccess() {
UserDto userDto = new UserDto();
userDto.setName("admin");
userDto.setPassword("123456");
Mockito.when(userService.getUserByName(Mockito.anyString())).thenReturn(userDto);
boolean result = loginService.login(userDto.getName(), userDto.getPassword());
Assert.assertTrue("登录功能验证失败 "+userDto, result);
}

@Test
public void loginFailed() {
UserDto userDto = new UserDto();
userDto.setName("admin");
userDto.setPassword("123456");
Mockito.when(userService.getUserByName(Mockito.anyString())).thenReturn(userDto);
//此处故意将密码设置错
boolean result = loginService.login(userDto.getName(), "1234561");
Assert.assertFalse("登录功能验证失败", result);
}

mock功能的好处在于:

  1. 简单易用,速度快;

  2. 不用依赖外部环境;

  3. 可重复测试,无副作用,不像DB或外部可能会产生持久性状态;

4. 使用DB的不持久化方案测试

简单来说这种方案其实就是配置事务,执行完UT让事务回滚掉,不产生持久状态;Demo: 还是以上边的登录为例BaseJunit,主要用于加载基础和通用注解:这里有一个非常重要的注解 @Rollback 即表示如果有事务最后都回滚

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath*:context/*.xml"})
@Rollback
public class BaseJunit {

}

LoginServiceImplTest继承了BaseJunit,实例化,使用@Autowired注解即可

public class LoginServiceImplTest extends BaseJunit {
@Autowired
private ILoginService loginService;

@Autowired
private IUserService userService;

/**
* 代码里写死了只有admin能登录成功,因此我们使用admin用户登录;
* 假设:使用admin预期结果成功
* 1. 准备数据
* 2. 使用userService插入一条数据
* 3. 调用登录接口
* 4. 使用Assert验证
*/
@Transactional
// 此处隐藏了一个Rollback注解因为在BaseJunit上统一了这个注解,
// 主要是为了让大家知道我们最后数据会回滚的,
// @Rollback//
@Test
public void loginSuccess() throws Exception {
//准备数据
UserDto userDto = new UserDto();
userDto.setName("admin");
userDto.setPassword("123456");
userDto.setAge(20);
userService.addUser(userDto);
//登录
boolean result = loginService.login(userDto.getName(), userDto.getPassword());
//验证
Assert.assertTrue("登录功能验证失败 " + userDto, result);
}

@Transactional
@Test
public void loginFailed() throws Exception {
//准备数据
UserDto userDto = new UserDto();
userDto.setName("admin");
userDto.setPassword("123456");
userDto.setAge(20);
userService.addUser(userDto);
boolean result = loginService.login(userDto.getName(), "1234561");
Assert.assertFalse("登录功能验证失败"+ userDto, result);
}

}

配置文件:context-all.xml

<?xml version="1.0" encoding="UTF-8"?>
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">















效果:在执行UT期间会在当前事务生成一条记录,当前UT验证可通过,且数据最后会自动回滚不落库;

这种方式相对于mock的优缺点:

优点

  1. 一定程度上可以验证DB层是否OK,当然如果是soa或是联调别人的接口就比较麻烦了

  2. 有时不用像mock一样造那么多数据,直接通过DB查询即可

缺点

  1. 依赖DB环境,也需要维护DB环境(甚至还有数据)

  2. 加载速度较慢,往往需要加载整个配置文件才能执行UT,优化UT启动配置成本并不低;

小结

UT的一般步骤

  1. 提出假设的输入

  2. 执行测试方法

  3. 验证预期结果(assert)
UT的重要指标和作用
  1. 所有的方法都验证通过

  2. 代码的覆盖率最好是100%

  3. 应达到可重复执行,可回归验证

最后个人经验

  1. UT可以大大提升工程师的代码质量,可大大减少逻辑性bug;

  2. 写UT习惯反过来可以大提升对代码重构水平;

  3. UT的回归测试可以及时反馈被改错的代码,这一点非常有用;可以考虑集成在cicd,上线需要UT没达到一定的代码覆盖率等

  4. 无状态的Mock测试往往就是最佳选择,但如果有需要,其实多种测试都可以一起使用;
c34095c1511e281b122acb82c6b482cf.png

近期好文推荐:

专访中行软件中心张新 | DevOps 新实践,女性如何在 IT 工作中撑起半边天?

遇见里斯本,走进 DevOps World & Jenkins World 葡萄牙站!

面试字节跳动,我被面试官狂怼全过程!

相爱相杀的运维之殤:苏宁消费金融超大规模 IT 系统 DevOps 实践

“DevOps时代”公众号诚邀广大技术人员投稿。

投稿邮箱:jiachen@greatops.net 或 添加联系人微信:135 2116 9787(同微信)。

点击,访问大会官网

d6e277ad4e1493593e9f9e8b6fb7fabf.png你点的每个赞,我都认真当成了喜欢
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值