前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >利用 Junt 维护代码质量

利用 Junt 维护代码质量

作者头像
DevOps时代
发布2020-02-11 17:38:18
5890
发布2020-02-11 17:38:18
举报

一、写本文背景

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

  1. 开发人员并不喜欢写UT,其中有包括很多资深工程师和架构师等;
  2. 写UT仅是为了验证即时代码的正确性,因此让 UT 变成了一次性的,且只为了本次代码的覆盖而写;

二、简单栗子热热身

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

代码语言:javascript
复制
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(被除数为质数情况,主要是验证不能整除的情况) …(当然还有其它的假设和预期结果)
代码语言:javascript
复制
@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);
    }

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

针对测试类或方法覆盖率

三、工程师并不喜欢写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,不可能拔网线关网络来实现吧:)

代码语言:javascript
复制
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,一般业务逻辑的代码针对这么多的异常也不可能一一处理,其实很多异常是可以合并处理的,如果不需要特殊处理的异常,可以统一起这些异常;

代码语言:javascript
复制
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代码如下:

代码语言:javascript
复制
@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掉

代码语言:javascript
复制
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 即表示如果有事务最后都回滚

代码语言:javascript
复制
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath*:context/*.xml"})
@Rollback
public class BaseJunit {

}

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

代码语言:javascript
复制
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

代码语言:javascript
复制
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       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">

    <!-- 需要扫描实例化注解的包名 -->
    <context:component-scan base-package="me.ele.ebu"/>

    <bean id="testDataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <!-- 基本属性 url、user、password -->
        <property name="url" value="jdbc:mysql://127.0.01:3306/test?useUnicode=true"/>
        <property name="username" value="test"/>
        <property name="password" value="test"/>
    </bean>

    <bean id="testSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="testDataSource"></property>
        <property name="mapperLocations" value="classpath*:/me/ele/ebu/junit/mapper/*Mapper.xml"/>
    </bean>

    <bean id="testMapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="me.ele.ebu.junit.mapper"/>
        <property name="sqlSessionFactoryBeanName" value="testSqlSessionFactory"/>
    </bean>

    <!-- 配置事务管理器 -->
    <tx:annotation-driven transaction-manager="testTxManager"  />

    <bean id="testTxManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="testDataSource"/>
    </bean>

</beans>

效果:在执行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测试往往就是最佳选择,但如果有需要,其实多种测试都可以一起使用;
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-12-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 DevOps时代 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、写本文背景
  • 二、简单栗子热热身
  • 三、工程师并不喜欢写UT的原因
  • 四、写UT的几个难点
  • 五、如何真正的使用UT达到我们的要求
    • 1.开始很痛苦,但熟能生巧
      • 2.能提升重构水平
        • 3.多使用Mock测试减少状态规避外部依赖
          • 4. 使用DB的不持久化方案测试
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档