做网站需要参考书目书,wordpress 固定链接 插件,台州宇洋台州网站建设,上海人才网官网查询概述 在今天#xff0c; 前后端分离已经是首选的一个开发模式。这对于后端团队来说其实是一个好消息#xff0c;减轻任务并且更专注。在测试方面#xff0c;就更加依赖于单元测试对于API以及后端业务逻辑的较验。当然单元测试并非在前后端分离流行之后才有#xff0c;它很早… 概述 在今天 前后端分离已经是首选的一个开发模式。这对于后端团队来说其实是一个好消息减轻任务并且更专注。在测试方面就更加依赖于单元测试对于API以及后端业务逻辑的较验。当然单元测试并非在前后端分离流行之后才有它很早就存在只是鲜有人重视且真的能够用好它。而在前后端分离开发模式下特别是两者交付时间差别很大的情况时后端可能需要更加地依赖于单元测试来保证代码的正确性。 本文主要围绕单元测试展开从单元测试的基础概念说起对比单元测试和集成测试同时我们还会聊一聊单元测试与测试驱动开发的区别。在我们了解完单元测试的概念之后我们会探讨一下什么样的单元测试算得上是好的单元测试它们具备哪些特征如何使用隔离框架来帮助我们对一些复杂的组件进行测试。最后一个内容也是本文想要阐述的重点 单元测试是开发人员写的那么开发人员在写自己的代码的时候如何提高自己代码的可测试性 什么样的代码算的上是对单元测试友好的代码 带着这些问题我们这就来开始我们的单元测试之旅。 什么是单元测试 有人可能写过单元测试但是却不知道为什么要写单元测试有人知道为什么要写单元测试但不确定如何写才是好的单元测试。但是对于“测试” 我们每个人都轻车熟路 你看看下面的功能是否似曾相识 单元测试与测试 测试种类分为很多种单元测试、集成测试、系统测试、压力测试、负载测试、验收测试等等 我们今天不打算也不能进行系统性的介绍。作为开发人员我们平常所说的“测试”。也就是说你代码写完了老大问你测试通过了吗你说过了然后就可以Check in 代码了。这里的“测试”实际上指的是不完整的功能测试。为什么说它不完整是因为从专业测试的角度来讲还需要定义规范的测试用例用例写完之后还要开发和测试人员一起评审等等 。 而我们只是在脑海中预想了一下它应该如何工作的应该给我什么结果等然后运行一下咦还真是这样的那我们的测试就算通过了。 会有多少Bug就取决于我们这个预想有多细了往往有时候我们只能想到很少一部份这时候专业独立的测试人员就派上用场了。同时精通开发和测试的人是很有优势的自己能够保证写出来的软件的质量这也是现代敏捷开发团队所追求的但是这样的人总是少之又少。 单元测试是通过把一个应用程序拆分成可测试的足够小的部分然后把每一部分与其它所有功能隔离开单独对这一部分进行测试。而这个“可测试的足够小的部分”就称之为“单元“在C语言中一个单元可以是一个函数在C#中单元测试可以是一个类。 如果所有的单元都能够像我们所预料的正常工作那么把他们合并起来就能够保证至少不会出现很严重的错误。 单元测试与集成测试 为什么要把这两项拿出来对比是因为这两项很容易混淆一不小心你就可能把单元测试写成集成测试了这也是为什么单元测试有时候看起来那么糟糕的主要原因。我们上面说单元测试是把每一个单元孤立出来在测试的时候不能和任何其它的单元有任何联系这是单元测试反过来你一旦在你的测试代码中引入了另外一个单元那你就要开始小心你是不是已经开始写集成测试了。 当然有时候往往不是引入了其它的一些单元有可能是一些组件下面列出了一些单元测试和集成测试的主要特点希望能够帮助大家区分单元测试与集成测试。 单元测试 可重复运行的持续长期有效并且返回一致的结果在内存中运行没有外部依赖组件比如说真实的数据库真实的文件存储等快速返回结果一个测试方法只测试一个问题 集成测试 利用真实的外部依赖采用真实的数据库外部的Web Service文件存储系统等在一个测试里面可能会多个问题数据库正常确配置系统逻辑等可以在运行较长时间之后才返回测试结果 单元测试与测试驱动开发TDD) 测试驱动开发其实我们用一个问题就可以解释清楚那就是“你什么时候写单元测试” 有人选择在开发的代码写完之后再写这样我们的开发过程是 理解需求-》编写代码-》针对代码结合需求写单元测试。后来大家发现往往在写单元测试的时候发现自己有些需求没有理解清楚或者这些需求原来设计的时候就没有考虑到所以又重新改原来的代码。 于是有人就说为什么我们不干脆反过来 先写单元测试再写代码 所以我们开发的过程就变成了这样理解需求-》针对需求写单元测试 -》 编写代码让单元测试通过。 最开始是叫测试先行TFD Test First Development) 后来就发展成我们熟知的测试驱动开发了。 测试驱动开发最大的好处是让开发人员更好的理解需求甚至是挖掘需求之后再进行开发。 当然我们不可能一次性把所有的测试代码都写出来之后再写代码这是一个重复迭代的过程 由于TDD不是我们本篇的主要内容这里仅仅希望能给大家一个对TDD的浅显认识的同时了解到TDD与单元测试的联系。到这里我们对于单元测试的概念就介绍的差不多了接下来是代码时间。:) 我们来上一个真实的例子更形象的了解一下单元测试。 一个单元测试的例子 那么问题来了我们用什么来案例来写了一个单元测试的例子呢既然这样那么我们就用前两篇我们在领域模型驱动设计中讲到的用户注册的例子吧。在用户的领域服务中UserService提供了一个Register的方法通过用户名、邮箱和密码三个参数来创建一个用户的对象。 像所有注册逻辑一样邮箱是不能重复的这是我们现在这个领域服务中比较重要的业务逻辑所以我们的单元测试必须要覆盖到。 我们的测试 namespace RepositoryAndEf.Domain.Tests { public class UserServiceTests { private IRepositoryUser _userRepository new MockRepositoryUser(); [Fact] public void RegisterUser_ExpectedParameters_Success() { var userService new UserService(_userRepository); var registeredUser userService.Register( hellojesseliuoutlook.com, Jesse, Jesse); var userFromRepository _userRepository.GetById(registeredUser.Id); userFromRepository.Should().NotBe(null); userFromRepository.Email.Should().Be(hellojesseliuoutlook.com); userFromRepository.Name.Should().Be(Jesse); userFromRepository.Password.Should().Be(Jesse); } [Fact] public void RegisterUser_ExistedEmail_ThrowException() { var userService new UserService(_userRepository); var registeredUser userService.Register( hellojesseliuoutlook.com, Jesse, Jesse); var userFromRepository _userRepository.GetById(registeredUser.Id); userFromRepository.Should().NotBe(null); Action action () userService.Register( hellojesseliuoutlook.com, Jesse_01, Jesse); action.ShouldThrowArgumentException(); } public void RegisterUser_ExistedName_ThrowException() { var userService new UserService(_userRepository); var registeredUser userService.Register( hellojesseliuoutlook.com, Jesse, Jesse); var userFromRepository _userRepository.GetById(registeredUser.Id); userFromRepository.Should().NotBe(null); Action action () userService.Register( hellojesseliu_02outlook.com, Jesse, Jesse); action.ShouldThrowArgumentException(); } } } 在这个例子中我们用到了 Fluentassertions、XUnit这两个开源组件。另外Moq作为一个不错的单元测试Mock框架也推荐给大家。 Fluentassertions相对于.NET测试工具本身提供的AssertFluentassertions提供基于链式构建的一些更人性、易懂的方法来帮助写出更好理解的单元测试代码 。 上面代码中我们所用到的ShoudBe、NotBe、以及ShoudThrow等方法即来自于Fluentassertions还有更多方法可以到官方文档上查询。Xunit这是一个开源的单元测试工具Moq为了让单元测试可以完全脱离外部组件我们需要用到一些Mock对象和Stub对象而Moq是一个开源的Mock类框架可以帮助我们实现这些功能 。我们上面代码中用到的MockRepository是我们自己用List封装的一个IRepository实例支持增删改查相当于我们把数据持久化于内存中。 namespace RepositoryAndEf.Data { public class MockRepositoryT : IRepositoryT where T : BaseEntity { private ListT _list new ListT(); public T GetById(Guid id) { return _list.FirstOrDefault(e e.Id id); } public IEnumerableT Get(ExpressionFuncT, bool predicate) { return _list.Where(predicate.Compile()); } public bool Insert(T entity) { if (GetById(entity.Id) ! null) { throw new InvalidCastException(The id has already existed); } _list.Add(entity); return true; } public bool Update(T entity) { var existingEntity GetById(entity.Id); if (existingEntity null) { throw new InvalidCastException(Cannot find the entity.); } existingEntity entity; return true; } public bool Delete(T entity) { var existingEntity GetById(entity.Id); if (existingEntity null) { throw new InvalidCastException(Cannot find the entity.); } _list.Remove(entity); return true; } } } MockRepository.cs 我们也可以用Moq框架在单元测试中临时初始化一个MockRepository private readonly IRepositoryUser _userRepository; private ListUser _userList new ListUser(); public UserServiceTests() { var mockRepository new MockIRepositoryUser(); // 初始化新增方法 mockRepository.Setup(r r.Insert(It.IsAnyUser())).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; } 在单元测试代码中临时初始化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级别的项目可能外面的架子、整体架构图画出来那是非常的漂亮但是一旦涉及到业务层面的代码....后面我就不说了。 整体架构层面的考虑 如果我们现在是重新开始搭建一套系统那我们可以怎样开始或者说如果我们有魄力和决心去重构一套系统我们该往哪些方向去走—— 从DDD的分层架构说起 分层 首先是通过分层把业务与其它基础组件隔离开不要让一些发邮件、记日志、写文件等这些基础组件混合了我们的业务在应用层将领域业务与这些为应用服务的基础功能组合起来。在之前的一篇文章 《初探领域驱动设计——为复杂业务而生》有具体的介绍。 领域业务层无依赖 在洋葱架构中核心Core层是与领域或技术无关的基础构件块它包含了一些通用的构件块例如list、case类或Actor等等。核心层不包含任何技术层面的概念例如REST或数据库等等。 如果有依赖请依赖于接口抽象而非具体的实现比如我们例子中的IRepository。这些架构思想其实已经很老很老了但是我们多数的项目还停留在更更老的三层架构思想上说好的技术极客们都去哪里了 保持类的引用/依赖关系清晰可注入 不要使用静态方案 且不要说一些面向对象的特性没有办法使用到一旦开了这个口子。天知道你的代码里面会依赖于多少个外部静态方法并且完全没有办法在测试代码中将它们mock掉万一你在静态方法里面又有其它依赖那对于单元测试来说就是一场终结。 保持一个类所有的外部引用易见 1. 所有外部引用易见 2. 外部引用可注入/替换 除了构造函数注入以外我们还可以采用构造函数注入、字段、以及方法注入的方式将我们的方法替换掉。这种方式不仅仅是对单元测试友好更是一种良好的代码组织方式是可能提供代码的易读性以及可维护性的。要知道代码主要是给人阅读的只是偶尔让机器执行一下。如果有跳槽经验的同学应该都有过那种到了一个公司有一个很复杂的系统但是没有任何的文档稍微好一点的可能会有表字典的感受唯一了解系统业务的方式是play with the system 然后看代码。 对于种无法一眼看到各个类之间的关系的代码特别是一个类里面有好几百个方法、上万行代码的时候 虽然我对于干这种事情已经轻车熟路但当时的心情难免还是有些激操动蛋的。 依赖于接口/抽象而非实现 这点我想也就不需要细述了在单元测试这个场景里面。我们主要是将业务与非业务相关功能用接口隔离开那么我们在单元测试中就可以很灵活的用Mock或者Stub来替换。比如读写文件、访问数据库、远程请求等等。 最后 编写单元测试虽然简单但是考验的却是细心和对业务的理解程度。而且往往写单元测试代码所花的时间比写功能代码还要多在任务时间进度紧、又不受重视的情况下自己很少有人会主动愿意去写。但是好的单元测试代码确实在长期能够体现出它的价值。 原文地址http://www.cnblogs.com/jesse2013/p/magic-of-unittesting.html .NET社区新闻深度好文微信中搜索dotNET跨平台或扫描二维码关注