在今天的文章中打算和大家聊一聊关于测试的话题, 也许有朋友会问, 作为一名码农为什么要关注测试的问题? 我们把代码开发完基本自测没问题了, 扔给测试不就行了? 有问题再改呗! 也许有很多人都会这么想, 的确, 目前国内很多程序员并不太关注 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 的一般步骤如下:
模拟任何外部第三方组件依赖, 并将这些模拟对象插入测试代码;
执行测试中的代码;
验证代码是否按照预期执行;
如果我们在 Spring Boot 的工程中引入了测试依赖 Jar, 实际上就已经引入了 Junit 及 Mockito 这两组测试框架的依赖. 如下:
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-test</artifactId>
- <scope>test</scope>
- </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 的好习惯, 做一个注重代码品质的优秀程序员! 希望大家都能够越变越优秀, 加油!
来源: http://www.jianshu.com/p/c55a93cd4cc1