您的位置:首页 > 其它

前后端分离开发模式下后端质量的保证 —— 单元测试

2016-06-14 08:56 609 查看

概述

  在今天, 前后端分离已经是首选的一个开发模式。这对于后端团队来说其实是一个好消息,减轻任务并且更专注。在测试方面,就更加依赖于单元测试对于API以及后端业务逻辑的较验。当然单元测试并非在前后端分离流行之后才有,它很早就存在,只是鲜有人重视且真的能够用好它。而在前后端分离开发模式下,特别是两者交付时间差别很大的情况时,后端可能需要更加地依赖于单元测试来保证代码的正确性。

  本文主要围绕单元测试展开,从单元测试的基础概念说起,对比单元测试和集成测试,同时我们还会聊一聊单元测试与测试驱动开发的区别。在我们了解完单元测试的概念之后,我们会探讨一下什么样的单元测试算得上是好的单元测试,它们具备哪些特征,如何使用隔离框架来帮助我们对一些复杂的组件进行测试。最后一个内容也是本文想要阐述的重点: 单元测试是开发人员写的,那么开发人员在写自己的代码的时候,如何提高自己代码的可测试性? 什么样的代码算的上是对单元测试友好的代码? 带着这些问题,我们这就来开始我们的单元测试之旅。

目录

什么是单元测试?

单元测试与测试

单元测试与集成测试

单元测试与测试驱动开发

一个单元测试的例子

Mock和Stub的区别

怎么样才算好的单元测试?
测试用例都有哪些?

自动化——持续集成

提高代码的可测试性
整体架构层面的考虑

保持类的引用/依赖关系清晰,可注入

依赖于接口而非实现

什么是单元测试?

  有人可能写过单元测试,但是却不知道为什么要写单元测试,有人知道为什么要写单元测试,但不确定如何写才是好的单元测试。但是对于“测试” 我们每个人都轻车熟路, 你看看下面的功能是否似曾相识?

private readonly IRepository<User> _userRepository;
private List<User> _userList = new List<User>();
public UserServiceTests()
{
var mockRepository = new Mock<IRepository<User>>();

// 初始化新增方法
mockRepository.Setup(r => r.Insert(It.IsAny<User>())).Returns((User user) =>
{
if (_userList.Any(u => u.Id == user.Id))
{
throw new InvalidCastException("The id has already existed");
}

_userList.Add(user);
return true;
});

_userRepository = mockRepository.Object;
}


View Code
  在单元测试代码中临时初始化Mock repository

更灵活:可以只初始化用到的方法

更强的控制能力:可以从外部(单元测试代码内)定义所有的行为

多态性:与其它单元测试类隔离,可以有不同的行为

Mock和Stub的区别

  因为有很多测试框架把Mock和Stub区别对待,初学者也会对这两个概念表示含糊不清。简单的来说,Mock与 Stub最大的区别是:

  Stub主要用来隔离其它的组件让单元测试可以正常的进行,我们不会对Stub来进行Assert。



  Mock则用来和测试代码进行交互,可以说我们会针对Mock来写测试代码,也会对它进行 Assert来验证我们的代码。

  在我们上面的代码中,我们只用到了一个Mock(MockRepository),如果同样是用户注册的业务,有哪些地方是我们可能需要用到Stub的? 试想一下现实的注册场景,如果用户注册成功了, 我们是不是需要给用户发送注册成功的邮件通知?这里有一点需要注意的是,注册用户相关的代码属于我们领域服务的职责,但是注册成功发送邮件、发送短信、甚至你要干一些系统相关的初始化操作都是属于应用层的事情。关于这点,大家还可以回顾之前的两篇关于DDD的文章。如果我们针对应用层的代码编写单元测试,那么我们就需要把一些组件比如邮件、日志等用Stub隔离掉,来保证测试代码的运行。

怎样才算好的单元测试?

什么是一个好的单元测试?

是自动化的和可重复运行的

很容易实现

持续有用

任何人只要轻松的点一下按钮就可以运行

运行不会花太长的时间

一直返回同样的结果(如果你不改变任何代码或参数)

单元测试是完全隔离的,不应该有任何其它的依赖

当单元测试失败的时候,应该一眼就看出是因为什么原因导致的这个失败

一个测试方法只验证一个case,只用一个Mock,Stub可以是多个

好的命名,最好是可以从方法名看出以下三个要素(所以一般我们采用三段命名法):

测试目标

条件

应该得到的结果

想知道你写的单元测试是不是好的单元测试么?

2个星期,或者2个月甚至2年前写的单元测试还能运行并且得到同样的结果么?

团队中的其它人也可以运行你2个月前写的单元测试么?

可以点击一下按钮就运行你所有的单元测试,并返回正确的结果么?

所有的单元测试可以在几分钟之内完成么?



测试用例都有哪些?

  写单元测试的代码可能是开发的好几倍,这句话是真的!在于你的单元测试用例覆盖的有多广,比如说我们上面针对用户注册这一个业务场景写了3个测试用例,其实是远远不够的。

非预期的用例

  不管我们上面那个完全成功注册的用例,还是另外两个由于邮箱和名称重复而没有注册成功的用例。这三个用户都是预期的,如果是非预期的,比如:

如果邮箱地址不是一个正确格式的邮箱?

如果我邮箱不填?用户名不填?

边界测试

如果我的邮箱名称或者用户名长度超过最大限制?

回归测试

  修改bug是一件难过的事情,在复杂且耦合度很高的系统下修改bug是一件难过且胆破心惊的事情,那么你感受一下:在复杂且耦合度很高的系统下不断的修改同一个bug会是一种什么样的心情。我们后期维护代码的时候对于新增的改动也需要加上对应的测试代码来保证单元测试的完整性。

自动化——持续集成

  持续集成里面已经包含了单元测试的自动化。它倡导团队开发成员必须经常集成他们的工作,甚至每天都可能发生多次集成。而每次的集成都是通过自动化的构建来验证,包括自动编译、发布和测试,从而尽快地发现集成错误,让团队能够更快的开发内聚的软件。感兴趣的同学可以自行了解,这是一个关于DevOps的话题,就不在本文作过多的表述。光想象一下那种不管谁有代码check in都引发所有单元测试代码的自动运行,在单元测试覆盖的全的情况下基本可以过滤掉很多的潜在bug。

提高代码的可测试性

  我们多数遇到的项目之所有很少看到单元测试的代码大概是因为以下的几个原因:

领导不重视 ,团队内没有这个风气

项目太紧,根本不给时间(可能也有领导不重视的原因)

开发人员对于单元测试不熟悉 ,不知道怎么样写好单测试。(不好的单元测试代码,写了可能等于白写,因为根本没人去运行它们)

解决方案里面的业务层根本没有办法写单元测试(耦合度太高,重依赖,这是当我排除前面3个困难之后,常常遇到的最后一道坎)

  关于最后一点是需要架构师、或者比较有经验在开发者在最开始设计系统结构的时候需要考虑到的。如果最开始没有考虑到怎么办? 那太好了,因为很多项目最开始都没有考虑到,所以我们的单元测试代码总是盛行不起来。(可怜这一层面的架构师也是少之又少,倒是有很多架构师活跃于各大论坛讲高并发、各种分布式组件,能挽起袖子去重构/优化代码结构的人真的少之又少。因为实在太累,而且搞不好还容易出错,属于最有挑战,但其实却往往不被老板重视的一项苦差事)遇到比较多的问题(包括BAT级别的项目,可能外面的架子、整体架构图画出来那是非常的漂亮,但是一旦涉及到业务层面的代码....后面我就不说了。)

整体架构层面的考虑

  如果我们现在是重新开始搭建一套系统,那我们可以怎样开始?或者说如果我们有魄力和决心去重构一套系统,我们该往哪些方向去走?—— 从DDD的分层架构说起

  分层: 首先是通过分层把业务与其它基础组件隔离开,不要让一些发邮件、记日志、写文件等这些基础组件混合了我们的业务,在应用层将领域业务与这些为应用服务的基础功能组合起来。在之前的一篇文章 《初探领域驱动设计——为复杂业务而生》有具体的介绍。



  领域业务层无依赖

  在洋葱架构中,核心(Core)层是与领域或技术无关的基础构件块,它包含了一些通用的构件块,例如list、case类或Actor等等。核心层不包含任何技术层面的概念,例如REST或数据库等等。

  


  如果有依赖,请依赖于接口抽象,而非具体的实现,比如我们例子中的IRepository。这些架构思想其实已经很老很老了,但是我们多数的项目还停留在更更老的三层架构思想上,说好的技术极客们都去哪里了?

保持类的引用/依赖关系清晰,可注入

  不要使用静态方案

  且不要说一些面向对象的特性没有办法使用到,一旦开了这个口子。天知道你的代码里面会依赖于多少个外部静态方法,并且完全没有办法在测试代码中将它们mock掉,万一你在静态方法里面又有其它依赖,那对于单元测试来说就是一场终结。

  保持一个类所有的外部引用易见

  1. 所有外部引用易见
  2. 外部引用可注入/替换

  



  除了构造函数注入以外,我们还可以采用构造函数注入、字段、以及方法注入的方式,将我们的方法替换掉。这种方式不仅仅是对单元测试友好,更是一种良好的代码组织方式,是可能提供代码的易读性,以及可维护性的。要知道代码主要是给人阅读的,只是偶尔让机器执行一下。如果有跳槽经验的同学应该都有过那种到了一个公司,有一个很复杂的系统,但是没有任何的文档(稍微好一点的可能会有表字典)的感受,唯一了解系统业务的方式是play with the system 然后,看代码。 对于种无法一眼看到各个类之间的关系的代码,特别是一个类里面有好几百个方法、上万行代码的时候, 虽然我对于干这种事情已经轻车熟路,但当时的心情难免还是有些激(操)动(蛋)的。

依赖于接口/抽象,而非实现

  这点我想也就不需要细述了,在单元测试这个场景里面。我们主要是将业务与非业务相关功能用接口隔离开,那么我们在单元测试中就可以很灵活的用Mock或者Stub来替换。比如:读写文件、访问数据库、远程请求等等。

最后

  编写单元测试虽然简单,但是考验的却是细心和对业务的理解程度。而且往往写单元测试代码所花的时间比写功能代码还要多,在任务时间进度紧、又不受重视的情况下,自己很少有人会主动愿意去写。但是,好的单元测试代码确实在长期能够体现出它的价值。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: