在今天 前后端分离已经是首选的一个开发模式。这对于后端团队来说其实是一个好消息减轻任务并且更专注。在测试方面就更加依赖于单元测试对于API以及后端业务逻辑的较验。当然单元测试并非在前后端分离流行之后才有它很早就存在只是鲜有人重视且真的能够用好它。而在前后端分离开发模式下特别是两者交付时间差别很大的情况时后端可能需要更加地依赖于单元测试来保证代码的正确性。本文主要围绕单元测试展开从单元测试的基础概念说起对比单元测试和集成测试同时我们还会聊一聊单元测试与测试驱动开发的区别。在我们了解完单元测试的概念之后我们会探讨一下什么样的单元测试算得上是好的单元测试它们具备哪些特征如何使用隔离框架来帮助我们对一些复杂的组件进行测试。最后一个内容也是本文想要阐述的重点 单元测试是开发人员写的那么开发人员在写自己的代码的时候如何提高自己代码的可测试性 什么样的代码算的上是对单元测试友好的代码 带着这些问题我们这就来开始我们的单元测试之旅。目录什么是单元测试单元测试与测试单元测试与集成测试单元测试与测试驱动开发一个单元测试的例子Mock和Stub的区别怎么样才算好的单元测试测试用例都有哪些自动化——持续集成提高代码的可测试性整体架构层面的考虑保持类的引用/依赖关系清晰可注入依赖于接口而非实现什么是单元测试有人可能写过单元测试但是却不知道为什么要写单元测试有人知道为什么要写单元测试但不确定如何写才是好的单元测试。但是对于“测试” 我们每个人都轻车熟路 你看看下面的功能是否似曾相识单元测试与测试测试种类分为很多种单元测试、集成测试、系统测试、压力测试、负载测试、验收测试等等 我们今天不打算也不能进行系统性的介绍。作为开发人员我们平常所说的“测试”。也就是说你代码写完了老大问你测试通过了吗你说过了然后就可以Check in 代码了。这里的“测试”实际上指的是不完整的功能测试。为什么说它不完整是因为从专业测试的角度来讲还需要定义规范的测试用例用例写完之后还要开发和测试人员一起评审等等 。 而我们只是在脑海中预想了一下它应该如何工作的应该给我什么结果等然后运行一下咦还真是这样的那我们的测试就算通过了。 会有多少Bug就取决于我们这个预想有多细了往往有时候我们只能想到很少一部份这时候专业独立的测试人员就派上用场了。同时精通开发和测试的人是很有优势的自己能够保证写出来的软件的质量这也是现代敏捷开发团队所追求的但是这样的人总是少之又少。单元测试是通过把一个应用程序拆分成可测试的足够小的部分然后把每一部分与其它所有功能隔离开单独对这一部分进行测试。而这个“可测试的足够小的部分”就称之为“单元“在C语言中一个单元可以是一个函数在C#中单元测试可以是一个类。 如果所有的单元都能够像我们所预料的正常工作那么把他们合并起来就能够保证至少不会出现很严重的错误。单元测试与集成测试为什么要把这两项拿出来对比是因为这两项很容易混淆一不小心你就可能把单元测试写成集成测试了这也是为什么单元测试有时候看起来那么糟糕的主要原因。我们上面说单元测试是把每一个单元孤立出来在测试的时候不能和任何其它的单元有任何联系这是单元测试反过来你一旦在你的测试代码中引入了另外一个单元那你就要开始小心你是不是已经开始写集成测试了。 当然有时候往往不是引入了其它的一些单元有可能是一些组件下面列出了一些单元测试和集成测试的主要特点希望能够帮助大家区分单元测试与集成测试。单元测试可重复运行的持续长期有效并且返回一致的结果在内存中运行没有外部依赖组件比如说真实的数据库真实的文件存储等快速返回结果一个测试方法只测试一个问题集成测试利用真实的外部依赖采用真实的数据库外部的Web Service文件存储系统等在一个测试里面可能会多个问题数据库正常确配置系统逻辑等可以在运行较长时间之后才返回测试结果单元测试与测试驱动开发TDD)测试驱动开发其实我们用一个问题就可以解释清楚那就是“你什么时候写单元测试” 有人选择在开发的代码写完之后再写这样我们的开发过程是 理解需求-》编写代码-》针对代码结合需求写单元测试。后来大家发现往往在写单元测试的时候发现自己有些需求没有理解清楚或者这些需求原来设计的时候就没有考虑到所以又重新改原来的代码。 于是有人就说为什么我们不干脆反过来 先写单元测试再写代码 所以我们开发的过程就变成了这样理解需求-》针对需求写单元测试 -》 编写代码让单元测试通过。 最开始是叫测试先行TFD Test First Development) 后来就发展成我们熟知的测试驱动开发了。测试驱动开发最大的好处是让开发人员更好的理解需求甚至是挖掘需求之后再进行开发。 当然我们不可能一次性把所有的测试代码都写出来之后再写代码这是一个重复迭代的过程由于TDD不是我们本篇的主要内容这里仅仅希望能给大家一个对TDD的浅显认识的同时了解到TDD与单元测试的联系。到这里我们对于单元测试的概念就介绍的差不多了接下来是代码时间。:) 我们来上一个真实的例子更形象的了解一下单元测试。一个单元测试的例子那么问题来了我们用什么来案例来写了一个单元测试的例子呢既然这样那么我们就用前两篇我们在领域模型驱动设计中讲到的用户注册的例子吧。在用户的领域服务中UserService提供了一个Register的方法通过用户名、邮箱和密码三个参数来创建一个用户的对象。 像所有注册逻辑一样邮箱是不能重复的这是我们现在这个领域服务中比较重要的业务逻辑所以我们的单元测试必须要覆盖到。 我们的测试// UserServiceTests.csView Code在这个例子中我们用到了 Fluentassertions、XUnit这两个开源组件。另外Moq作为一个不错的单元测试Mock框架也推荐给大家。Fluentassertions相对于.NET测试工具本身提供的AssertFluentassertions提供基于链式构建的一些更人性、易懂的方法来帮助写出更好理解的单元测试代码 。 上面代码中我们所用到的ShoudBe、NotBe、以及ShoudThrow等方法即来自于Fluentassertions还有更多方法可以到官方文档上查询。Xunit这是一个开源的单元测试工具Moq为了让单元测试可以完全脱离外部组件我们需要用到一些Mock对象和Stub对象而Moq是一个开源的Mock类框架可以帮助我们实现这些功能 。我们上面代码中用到的MockRepository是我们自己用List封装的一个IRepository实例支持增删改查相当于我们把数据持久化于内存中。MockRepository.cs我们也可以用Moq框架在单元测试中临时初始化一个MockRepositoryView Code在单元测试代码中临时初始化Mock repository更灵活可以只初始化用到的方法更强的控制能力可以从外部单元测试代码内定义所有的行为多态性与其它单元测试类隔离可以有不同的行为Mock和Stub的区别因为有很多测试框架把Mock和Stub区别对待初学者也会对这两个概念表示含糊不清。简单的来说Mock与 Stub最大的区别是Stub主要用来隔离其它的组件让单元测试可以正常的进行我们不会对Stub来进行Assert。Mock则用来和测试代码进行交互可以说我们会针对Mock来写测试代码也会对它进行 Assert来验证我们的代码。在我们上面的代码中我们只用到了一个MockMockRepository)如果同样是用户注册的业务有哪些地方是我们可能需要用到Stub的 试想一下现实的注册场景如果用户注册成功了 我们是不是需要给用户发送注册成功的邮件通知这里有一点需要注意的是注册用户相关的代码属于我们领域服务的职责但是注册成功发送邮件、发送短信、甚至你要干一些系统相关的初始化操作都是属于应用层的事情。关于这点大家还可以回顾之前的两篇关于DDD的文章。如果我们针对应用层的代码编写单元测试那么我们就需要把一些组件比如邮件、日志等用Stub隔离掉来保证测试代码的运行。怎样才算好的单元测试什么是一个好的单元测试是自动化的和可重复运行的很容易实现持续有用任何人只要轻松的点一下按钮就可以运行运行不会花太长的时间一直返回同样的结果如果你不改变任何代码或参数单元测试是完全隔离的不应该有任何其它的依赖当单元测试失败的时候应该一眼就看出是因为什么原因导致的这个失败一个测试方法只验证一个case只用一个MockStub可以是多个好的命名最好是可以从方法名看出以下三个要素所以一般我们采用三段命名法测试目标条件应该得到的结果想知道你写的单元测试是不是好的单元测试么2个星期或者2个月甚至2年前写的单元测试还能运行并且得到同样的结果么团队中的其它人也可以运行你2个月前写的单元测试么可以点击一下按钮就运行你所有的单元测试并返回正确的结果么所有的单元测试可以在几分钟之内完成么测试用例都有哪些写单元测试的代码可能是开发的好几倍这句话是真的在于你的单元测试用例覆盖的有多广比如说我们上面针对用户注册这一个业务场景写了3个测试用例其实是远远不够的。非预期的用例不管我们上面那个完全成功注册的用例还是另外两个由于邮箱和名称重复而没有注册成功的用例。这三个用户都是预期的如果是非预期的比如如果邮箱地址不是一个正确格式的邮箱如果我邮箱不填用户名不填边界测试如果我的邮箱名称或者用户名长度超过最大限制回归测试修改bug是一件难过的事情在复杂且耦合度很高的系统下修改bug是一件难过且胆破心惊的事情那么你感受一下在复杂且耦合度很高的系统下不断的修改同一个bug会是一种什么样的心情。我们后期维护代码的时候对于新增的改动也需要加上对应的测试代码来保证单元测试的完整性。自动化——持续集成持续集成里面已经包含了单元测试的自动化。它倡导团队开发成员必须经常集成他们的工作甚至每天都可能发生多次集成。而每次的集成都是通过自动化的构建来验证包括自动编译、发布和测试从而尽快地发现集成错误让团队能够更快的开发内聚的软件。感兴趣的同学可以自行了解这是一个关于DevOps的话题就不在本文作过多的表述。光想象一下那种不管谁有代码check in都引发所有单元测试代码的自动运行在单元测试覆盖的全的情况下基本可以过滤掉很多的潜在bug。提高代码的可测试性我们多数遇到的项目之所有很少看到单元测试的代码大概是因为以下的几个原因领导不重视 团队内没有这个风气项目太紧根本不给时间可能也有领导不重视的原因开发人员对于单元测试不熟悉 不知道怎么样写好单测试。不好的单元测试代码写了可能等于白写因为根本没人去运行它们解决方案里面的业务层根本没有办法写单元测试耦合度太高重依赖这是当我排除前面3个困难之后常常遇到的最后一道坎关于最后一点是需要架构师、或者比较有经验在开发者在最开始设计系统结构的时候需要考虑到的。如果最开始没有考虑到怎么办 那太好了因为很多项目最开始都没有考虑到所以我们的单元测试代码总是盛行不起来。可怜这一层面的架构师也是少之又少倒是有很多架构师活跃于各大论坛讲高并发、各种分布式组件能挽起袖子去重构/优化代码结构的人真的少之又少。因为实在太累而且搞不好还容易出错属于最有挑战但其实却往往不被老板重视的一项苦差事遇到比较多的问题包括BAT级别的项目可能外面的架子、整体架构图画出来那是非常的漂亮但是一旦涉及到业务层面的代码....后面我就不说了。