Photo :Unit Test in Visual Studio

文 | Edison Zhou

上一篇我们学习了如何使用模拟对象进行交互测试。这一篇我们则会进一步使用隔离框架支持适应未来和可用性的功能。

为何使用模拟框架?

对于复杂的交互场景,可能手工编写模拟对象和存根就会变得很不方便,因此,我们可以借助隔离框架来帮我们在运行时自动生成存根和模拟对象。

一个隔离框架是一套可编程的API,使用这套API创建伪对象比手工编写容易得多,快得多,而且简洁得多。

隔离框架的主要功能就在于帮我们生成动态伪对象,动态伪对象是运行时创建的任何存根或者模拟对象,它的创建不需要手工编写代码(硬编码)。

关于NSub框架

Nsubstitute是一个开源的框架,源码是C#实现的。你可以在这里获得它的源码:https://github.com/nsubstitute/NSubstitute

NSubstitute 更注重替代(Substitute)概念。它的设计目标是提供一个优秀的测试替代的.NET模拟框架。它是一个模拟测试框架,用最简洁的语法,使得我们能够把更多的注意力放在测试工作,减轻我们的测试配置工作,以满足我们的测试需求,帮助完成测试工作。它提供最经常需要使用的测试功能,且易于使用,语句更符合自然语言,可读性更高。对于单元测试的新手或只专注于测试的开发人员,它具有简单、友好的语法,使用更少的lambda表达式来编写完美的测试程序。

NSubstitute 采用的是Arrange-Act-Assert测试模式,你只需要告诉它应该如何工作,然后断言你所期望接收到的请求,就大功告成了。因为你有更重要的代码要编写,而不是去考虑是需要一个Mock还是一个Stub。

在.NET项目中,我们仍然可以通过NuGet来安装NSubsititute:

使用NSub框架

NSub是一个受限的框架,它最适合为接口创建伪对象。我们继续以前的例子,来看下面一段代码,它是一个手写的伪对象FakeLogger,它会检查日志调用是否正确执行。此处我们没有使用隔离框架。

    public interface ILogger{void LogError(string message);}public class FakeLogger : ILogger{public string LastError;public void LogError(string message){LastError = message;}}[Test]public void Analyze_TooShortFileName_CallLogger(){// 创建伪对象FakeLogger logger = new FakeLogger();MyLogAnalyzer analyzer = new Chapter5.MyLogAnalyzer(logger);analyzer.MinNameLength = 6;analyzer.Analyze("a.txt");StringAssert.Contains("too short", logger.LastError);}

现在我们看看如何使用NSub伪造一个对象,换句话说,之前我们手动写的FakeLogger在这里就不用再手动写了:

    [Test]public void Analyze_TooShortFileName_CallLogger(){// 创建模拟对象,用于测试结尾的断言ILogger logger = Substitute.For<ILogger>();MyLogAnalyzer analyzer = new MyLogAnalyzer(logger);analyzer.MinNameLength = 6;analyzer.Analyze("a.txt");// 使用NSub API设置预期字符串logger.Received().LogError("Filename too short : a.txt");}

需要注意的是:

(1)ILogger接口自身并没有这个Received方法;

(2)NSub命名空间提供了一个扩展方法Received,这个方法可以断言在测试中调用了伪对象的某个方法;

(3)通过在LogError()前调用Received(),其实是NSub在询问伪对象的这个方法是否调用过。

使用NSub模拟返回值

如果接口的方法返回不为空,如何从实现接口的动态伪对象返回一个值呢?我们可以借助NSub强制方法返回一个值:

    [Test]public void Returns_ByDefault_WorksForHardCodeArgument(){IFileNameRules fakeRules = Substitute.For<IFileNameRules>();// 强制方法返回假值fakeRules.IsValidLogFileName("strict.txt").Returns(true);Assert.IsTrue(fakeRules.IsValidLogFileName("strict.txt"));}

如果我们不想关心方法的参数,即无论参数是什么,方法应该总是返回一个价值,这样的话测试会更容易维护,因此我们可以借助NSub的参数匹配器:

    [Test]public void Returns_ByDefault_WorksForAnyArgument(){IFileNameRules fakeRules = Substitute.For<IFileNameRules>();// 强制方法返回假值fakeRules.IsValidLogFileName(Arg.Any<string>()).Returns(true);Assert.IsTrue(fakeRules.IsValidLogFileName("anything.txt"));}

Arg.Any<Type>称为参数匹配器,在隔离框架中被广泛使用,控制参数处理。

如果我们需要模拟一个异常,也可以借助NSub来解决:

    [Test]public void Returns_ArgAny_Throws(){IFileNameRules fakeRules = Substitute.For<IFileNameRules>();fakeRules.When(x => x.IsValidLogFileName(Arg.Any<string>())).Do(context => { throw new Exception("fake exception"); });Assert.Throws<Exception>(() => fakeRules.IsValidLogFileName("anything"));}

这里,使用了Assert.Throws验证被测试方法确实抛出了一个异常。When和Do两个方法顾名思义代表了什么时候发生了什么事,发生了事之后要触发其他什么事。需要注意的是,这里When方法必须使用Lambda表达式。

同时使用模拟对象与存根

这里我们在一个场景中结合使用两种类型的伪对象:一个用作存根,另一个用作模拟对象。

继续前面的一个例子,LogAnalyzer要使用一个MailServer类和一个WebService类,这次需求有变化:如果日志对象抛出异常,LogAnalyzer需要通知Web服务,如下图所示:

我们需要确保的是:如果日志对象抛出异常,LogAnalyzer会把这个问题通知WebService。下面是被测试类的代码:

    public interface IWebService{void Write(string message);}public class LogAnalyzerNew{private ILogger _logger;private IWebService _webService;public LogAnalyzerNew(ILogger logger, IWebService webService){_logger = logger;_webService = webService;}public int MinNameLength{get; set;}public void Analyze(string fileName){if (fileName.Length < MinNameLength){try{_logger.LogError(string.Format("Filename too short : {0}", fileName));}catch (Exception ex){_webService.Write("Error From Logger : " + ex.Message);}}}}

现在我们借助NSubstitute进行测试:

    [Test]public void Analyze_LoggerThrows_CallsWebService(){var mockWebService = Substitute.For<IWebService>();var stubLogger = Substitute.For<ILogger>();// 无论输入什么都抛出异常stubLogger.When(logger => logger.LogError(Arg.Any<string>())).Do(info => { throw new Exception("fake exception"); });var analyzer = new LogAnalyzerNew(stubLogger, mockWebService);analyzer.MinNameLength = 10;analyzer.Analyze("short.txt");//验证在测试中调用了Web Service的模拟对象,调用参数字符串包含 "fake exception"mockWebService.Received().Write(Arg.Is<string>(s => s.Contains("fake exception")));}

这里我们不需要手工实现伪对象,但是代码的可读性已经变差了,因为有一堆Lambda表达式,不过它也帮我们避免了在测试中使用方法名字符串。

小结

这一系列文章我们学习了单元测试的核心技术:存根、模拟对象以及隔离(Mock)框架。使用存根可以帮助我们破除依赖,模拟对象与存根的区别主要在于存根不会导致测试失败,而模拟对象则可以。要辨别你是否使用了存根,最简单的方法是:存根永远不会导致测试失败,测试总是对被测试类进行断言。使用隔离(Mock)框架,测试代码会更加易读、易维护,重点是可以帮助我们节省不少时间编写模拟对象和存根

参考资料

(1)Roy Osherove 著,金迎 译,《单元测试的艺术(第2版)》

(2)匠心十年,《NSubsititue完全手册》

(3)张善友,《单元测试模拟框架:NSubstitute》

2020后记:虽然这是一篇发表于2015年的文章,但我至今觉得仍有价值。因为我发现在.NET圈,还是有很多童鞋不了解单元测试和不喜欢写单元测试,不懂其价值就不会形成增强回路。所谓增强回路,就是我单元测试写的越多,以后修改代码增加功能就不容易出现Bug(这里主要指SIT阶段、UAT阶段乃至线上),越不容易出现Bug我提交的代码质量就越高,就会增强我写单元测试的愿望,形成一个回路。在我现在的实践中,是把单元测试加入了持续集成构建任务中的,每次组员提交代码都会触发构建任务,去编译项目,去跑单元测试,只要单元测试没有跑过就会邮件或者通知发出来告诉我,我会知道是谁提交的代码居然没有跑单元测试就提交了,我就会找他改Bug了,呵呵。

The End

「 码字不易,也希望各位看官看完觉得还行就在本文右下方顺手点个“在看”,那就是对我最大的鼓励!如果觉得很好,也可以转发给你的朋友,让更多人看到,独乐乐不如众乐乐,是吧?

往期精彩回顾

.NET Core on K8S学习与实践系列文章索引目录

.NET Core 微服务学习与实践系列文章索引目录

【资料】2019 .NET China Conf 大会资料下载

【视频】2019 .NET China Conf 大会视频发布

2019 .NET China Conf 路一直都在,社区会更好

基于Jenkins的开发测试全流程持续集成实践

基于Jenkins Pipeline的.NET Core持续集成实践

【导读】我读经典,心旷神怡 - 经典书籍读后感汇总

【导读】我的诗和远方 - 也读唐诗与旅游游记汇总

点个【在看】如何?

UnitTest in .NET(Part 4)相关推荐

  1. UnitTest in .NET(Part 1)

    Photo :Unit Test in Visual Studio 文 | Edison Zhou 2015年看了Roy Osherove的<单元测试的艺术>一书,颇有收获.因此,我在当时 ...

  2. UnitTest in .NET(Part 2)

    Photo :Unit Test in Visual Studio 文 | Edison Zhou 上一篇我们学习基本的单元测试基础知识和入门实例.但是,如果我们要测试的方法依赖于一个外部资源,如文件 ...

  3. python接口自动化(二十四)--unittest断言——中(详解)

    简介 上一篇通过简单的案例给小伙伴们介绍了一下unittest断言,这篇我们将通过结合和围绕实际的工作来进行unittest的断言.这里以获取城市天气预报的接口为例,设计了 2 个用例,一个是查询北京 ...

  4. python接口自动化(二十五)--unittest断言——下(详解)

    简介 本篇还是回归到我们最初始的话题,想必大家都忘记了,没关系看这里:传送门  没错最初的话题就是登录,由于博客园的登录机制改变了,本篇以我找到的开源免费的登录API为案例,结合 unittest 框 ...

  5. python unittest断言_python接口自动化(二十四)--unittest断言——中(详解)

    简介 上一篇通过简单的案例给小伙伴们介绍了一下unittest断言,这篇我们将通过结合和围绕实际的工作来进行unittest的断言.这里以获取城市天气预报的接口为例,设计了 2 个用例,一个是查询北京 ...

  6. python接口自动化(二十三)--unittest断言——上(详解)

    简介 在测试用例中,执行完测试用例后,最后一步是判断测试结果是 pass 还是 fail,自动化测试脚本里面一般把这种生成测试结果的方法称为断言(assert).用 unittest 组件测试用例的时 ...

  7. UnitTest in .NET(Part 5)

    Photo :UnitTesting 文 | Edison Zhou 上一篇我们学习了单元测试的核心技术:存根.模拟对象和隔离框架,它们是我们进行高质量单元测试的技术基础.本篇会集中在管理和组织单元测 ...

  8. python读取每一行文字二十四_python接口自动化(二十四)--unittest断言——中(详解)...

    简介 上一篇通过简单的案例给小伙伴们介绍了一下unittest断言,这篇我们将通过结合和围绕实际的工作来进行unittest的断言.这里以获取城市天气预报的接口为例,设计了 2 个用例,一个是查询北京 ...

  9. python自动化测试断言_python接口自动化(二十五)--unittest断言——下(详解)...

    本文转载自: https://www.cnblogs.com/du-hong/p/10766314.html 简介 本篇还是回归到我们最初始的话题,想必大家都忘记了,没关系看这里:传送门  没错最初的 ...

最新文章

  1. MySQL InnoDB表压缩
  2. MySQL—创建数据表
  3. Android自己的自动化测试Monkeyrunner和用法示例
  4. [安全模型][Cambria Math][A][]敌手A-> 怎么打出来?
  5. 【linux】RedHat 7.x 升级 openssh 为 8.x 版本
  6. 自己动手写Docker系列 -- 4.3实现volume数据卷
  7. DE21 Convolution Formula
  8. oracle 039 00 039,python+robot+oracle:执行脚本时中文sql报错:UnicodeEncodeError: #039;ascii#039; codec can#...
  9. powerquery加载pdf_老板让我汇总PDF文件,我不会,同事用Excel两分钟就搞定
  10. iOS比较两张图的相似度
  11. 浅析PWM控制电机转速的原理
  12. 浏览器打开是360导航页面解决方法
  13. 六年级计算机课件,六年级信息技术上册课件.ppt
  14. 移动机会网络中的节点分簇路由算法
  15. C++十一月月末总结
  16. 金句: 對比MBA學位,我們更需要PSD學位的人! Poor, Smart and Deep Desire to… | consilient_lollapalooza on Xanga...
  17. 元数据管理 开源项目技术选型
  18. 14个10G电口模块(10GBase-T)的相关问题
  19. MATLAB中不用循环生成圆盘(圆形)/圆环掩膜矩阵
  20. ERA5-Land hourly data数据直接计算出来数据量偏大,monthly单位等

热门文章

  1. SharePoint 2010 中的BCS身份验证模式
  2. 让Visual Studio 2013为你自动生成XML反序列化的类
  3. javascript基础修炼(4)——UMD规范的代码推演
  4. (三)Controller接口控制器详解(二)
  5. 简单获取任意app的URL Schemes
  6. 上周面试回来后写的Java面试总结,想进BAT必看
  7. 前端实现连连看小游戏(1)
  8. 如何部署同一个Spring boot web 应用到不同的环境
  9. 专题1.1——Exchange2013部署前准备条件
  10. 有关[Http持久连接]的一切,卷给你看