优秀程序员20%的时间都在写单元测试?

无敌码农 梦见 2019-07-07


导读


在今天的文章中打算和大家聊一聊关于测试的话题。也许有朋友会问,作为一名码农为什么要关注测试的问题?我们把代码开发完基本自测没问题了,扔给测试不就行了?有问题再改呗!也许有很多人都会这么想,的确,目前国内很多程序员并不太关注Unit Test,很多互联网公司也并没有强制要求开发人员必须编写Unit Test Case。究其原因,可能是国内公司都比较有钱,测试团队动辄几十人,甚至上百人的公司大有人在。所以,从很多程序员的心态上看,测试这么多,直接扔给他们测试就好了!而另外一个被提及的原因,则是国内互联网公司产品迭代速度太快,需求太多做不过来,那里有时间写Unit Test呢?


也许原因是多样的,但抛开各种各样的因素,今天我们只从程序员成长的角度来聊一聊该不该写Unit Test?最近这段时间和海外的程序员朋友合作开发项目比较多,给我的感受是他们特别强调Unit Test,用他们的话来说比较在意程序的品质。而反观国内很多公司这一点做的就并不是那么好了!之前也和他们聊过其中的原因,他们认为是国内最近这些年的发展太快了,以至于有些过程被跳过了。


我们知道开发一个软件或者平日里在现有的项目中开发某个需求时,严格来说一般会经历这么一个流程,如下图所示:



从图上可以看到,在这个流程中软件被交付集成测试之前,一定要先跑过Unit Test,而现在很多国内公司的测试流程都绕过Unit Test直接过度到集成测试和QA测试,而从客观的情况看,其实往往开发对逻辑是最了解的,如果开发可以通过覆盖相对完整的Unit Test的话,实际上后续测试流程就会顺利的多,而且写Unit Test还有一个好处,就是能够促使开发人员不断优化代码的设计逻辑,因为一旦你发现代码无法被Unit Test的时候,就说明你的代码不够组件化而需要被重构了!作为一名程序员,如果你能够在这种过程中不断地审视自己写过的代码,相信你的代码编写水平一定会得到不断地提高!


而从软件可维护性的角度看,Unit Test覆盖全面的项目往往都会比较好维护,因为完整的Unit Test实际上已经固化了软件当前的逻辑,一旦有人在后续的开发中破坏了这个逻辑,就会导致Unit Test无法通过,此时如果要求无法被Unit Test跑过的代码不能被编译成功或者提交的话,那么就会强迫修改者去完善Unit Test。这样也从侧面提高了程序员的测试意识,减少了发生重大Bug的几率!


从以上两个角度看,Unit Test一方面可以提高程序员的编码水平,另外一方面也能尽量保证软件的质量,所以Unit Test是一件非常有价值的事情,难怪他们说优秀的程序员20%的时间都在写Unit Test!


Unit Test该怎么写

在前面的内容中,我们讲到Unit Test是一件非常有价值的事情,那么在实际的项目中Unit Test到底该怎么写呢?以使用Spring Boot框架并基于Spring MVC开发的Web服务为例,大部分情况下的代码结构如图所示:




在这个软件结构中一般面向外部调用的是Controller层的服务接口定义,这一层由Spring MVC框架提供支持;而Controller层在接收到请求后需要将参数传递给Service层的业务方法进行处理,而Service层的业务方法逻辑就会比较多样,例如可能需要操作数据库就通过Dao层提供的组件去实现,也可能需要访问个中间件组件之类,如缓存服务Redis、消息服务RocketMQ之类。除此之外,Service层逻辑可能还会涉及到其他第三方服务的调用,例如支付业务还需要调用支付宝之类的接口等等!


所以一般来说Unit Test的重点就是Service层的业务逻辑方法,如果Controller层也涉及到一些流程逻辑之类,也需要被Unit Test覆盖一下!而具体的Unit Test用例编写,遵循Maven工程规约即可。


不过说到这里大家可能会有很大的疑问,那就是我们在进行Unit Test时,正如上图所示Service层本身依赖了很多其他组件,有些需要调用数据库、有些需要访问Redis、有些还需要调用第三方接口,在这种情况下好像很难让Unit Test跑下去,因为不可能每次运行Unit Test的时候这些环境都是在线的,怎么办呢?所以在早期写Unit Test,如果有第三方依赖无法被测试的情况下是需要我们手动编写Mock测试代码的,举个例子假设我们有个业务层的类class A{...}需要被Unit Test,但是A中依赖于第三方组件代码B,由于B需要连接外部网络,所以我们在测试A的时候没有办法直接依赖B的实例,所以我们一般来说需要单独定义个class MockB extend B{@Override ...},这个类继承B并以Mock的方式重写其方法,从而来为A类的Unit Test提供Mock Bean!而这种由于组件依赖复杂的情况,也在某种程度上限制来大家写Unit Test的热情,不过下面要介绍的这个神器会让这件事变得非常容易!


Unit Test神器之Mockito


在上面我们谈到了在编写业务层Unit Test时候会发现复杂的组件依赖需要我们编写很多额外的Mock类,增加来我们编写Unit Test的难度,而Mockito这个测试框架的出现则让Mock这件事变得非常容易了!Mockito是一个模拟测试框架,可以让我们以注解(@MockBean)的方式优雅地进行依赖组件的Mock并对执行逻辑进行验证。使用Mockito的一般步骤如下:



1、模拟任何外部第三方组件依赖,并将这些模拟对象插入测试代码;

2、执行测试中的代码;

3、验证代码是否按照预期执行;


如果我们在Spring Boot的工程中引入了测试依赖Jar,实际上就已经引入了Junit及Mockito这两组测试框架的依赖。如下:


<dependency>
     <groupId>org.springframework.bootgroupId>

     <artifactId>spring-boot-starter-testartifactId>
     <scope>testscope>
dependency>


下面我们以一个实际的案例来演示下如何编写一个针对Service层代码Unit Test,Service业务逻辑代码如下:


@Service
public class UserAccountTradeServiceImpl implements UserAccountTradeService {

    @Autowired
    WalletOrderDao walletOrderDao;

    @Autowired
    PaymentClient paymentClient;

    @Override
    public AccountChargeTradeResVo accountChargeTrade(AccountChargeTradeReqVo accountChargeTradeReqVo)
            throws Exception 
{
        //充值交易防重
        WalletOrder walletOrder = walletOrderDao.selectOrderById(accountChargeTradeReqVo.getOrderId());
        if (walletOrder != null) {
            throw new Exception("充值订单重复");
        }
        //构建充值订单
        walletOrder = WalletOrder.builder().orderId(accountChargeTradeReqVo.getOrderId())
                .userId(String.valueOf(accountChargeTradeReqVo.getUserId()))
                .amount(accountChargeTradeReqVo.getAmount())
                .busiType("0").tradeType("charge").currency(accountChargeTradeReqVo.getCurrency()).status("1")
                .isRenew(accountChargeTradeReqVo.getReNew()).tradeTime(new Timestamp(new Date().getTime()))
                .updateTime(new Timestamp(new Date().getTime()))
                .build();
        walletOrderDao.insertOrder(walletOrder);
        //调用支付接口
        paymentClient.consumeAccount(1"1""CNY");
        //构建返回参数
        AccountChargeTradeResVo accountChargeTradeResVo = AccountChargeTradeResVo.builder()
                .userId(Long.valueOf(walletOrder.getUserId())).currency(walletOrder.getCurrency())
                .orderId(walletOrder.getOrderId()).businessType(walletOrder.getBusiType()).build();
        return accountChargeTradeResVo;
    }
}


以上业务代码实际上是演示了一个用户钱包充值的大致逻辑的业务层方法,而该方法中有两个依赖组件需要被Mock一个是表示操作数据库的walletOrderDao,另外一个则是表示需要调用支付系统的客户端依赖paymentClient。那么使用Mockito该如何在Unit Test中进行Mock呢?


我们在工程对应的test目录的包结构中,建立一个与业务层逻辑包结构一样的测试代码结构,如下图所示:




一般来说Unit Test类的代码接口与实际源码结构一致就行,以被测试类+Test后缀命名即可。接下来我们编写该测试代码:


@RunWith(SpringRunner.class)
@SpringBootTest(classes = {UserAccountTradeServiceImpl.class})
//@ActiveProfiles({"test"})
public class UserAccountTradeServiceImplTest {

    @MockBean
    private WalletOrderDao walletOrderDao;

    @MockBean
    private PaymentClient paymentClient;

    @Autowired
    private UserAccountTradeServiceImpl userAccountTradeServiceImpl;

    @Test
    public void accountChargeTradeTest() throws Exception {
        AccountChargeTradeReqVo accountChargeTradeReqVo = AccountChargeTradeReqVo.builder().orderId("12345")
                .userId(1001).amount(1000).currency("CNY").tradeTime("2019070412102023").reNew("1").build();
        AccountChargeTradeResVo accountChargeTradeResVo = userAccountTradeServiceImpl
                .accountChargeTrade(accountChargeTradeReqVo);
        assertNotNull(accountChargeTradeResVo);
        assertEquals(accountChargeTradeResVo.getOrderId(), accountChargeTradeReqVo.getOrderId());
        given(paymentClient.consumeAccount(any(Long.class), any(String.class), any(String.class))).willReturn(null);
        verify(paymentClient).consumeAccount(any(Long.class), any(String.class), any(String.class));
    }
}


在以上测试代码中我们通过@MockBean这个注解就很容易的Mock了该业务层代码的依赖组件,这样测试代码在执行依赖组件的逻辑时就会被Mock而不会真正调用这个方法。而一般情况下我们也可以验证下Mock对象的方法是否有被调用,但是只是验证下调用本身是否触发而并不是真的调用,可以使用given/verify这两个Mocktio提供的方法来实现。


对于大部分情况采用这样的模式进行Unit Test就差不多了,更多其他细节的用法大家可以在好好研究下Mocktio提供的功能!在这里示例中还有个一个小的技巧,就是我们在使用@SpringBootTest的时候如:


@SpringBootTest(classes = {UserAccountTradeServiceImpl.class})


可以直接指定要测试的Service类,这样Spring Boot就不会加载其他乱七八糟的依赖了,这样会节约Unit Test运行的时间。


关于Unit Test还有很多其他的细节技巧,由于篇幅的关系暂时没法一一详细介绍。写这篇文章最主要的目的还在于希望大家养成写Unit Test的好习惯,做一个注重代码品质的优秀程序员。


—————END—————



别图片二维码,关注 无敌码农 获取精彩内容


欢迎扫码关注公众号 程序员内推圈,优秀的内推机会等着你!

    已同步到看一看

    发送中

    本站仅按申请收录文章,版权归原作者所有
    如若侵权,请联系本站删除
    觉得不错,分享给更多人看到
    梦见 热门文章:

    漫画:编程其实是文科    阅读/点赞 : 1108/17

    我是一个线程    阅读/点赞 : 693/22

    漫画:三分钟了解敏捷开发    阅读/点赞 : 644/17

    漫画:什么是中间人攻击    阅读/点赞 : 613/16

    我是一个Java class    阅读/点赞 : 552/10

    网贷遇雷潮,投资者如何避雷?    阅读/点赞 : 0/0