UnitTest in .NET(Part 2)


Photo :Unit Test in Visual Studio

文 | Edison Zhou

上一篇我们学习基本的单元测试基础知识和入门实例。但是,如果我们要测试的方法依赖于一个外部资源,如文件系统、数据库、Web服务或者其他难以控制的东西,那又该如何编写测试呢?为了解决这些问题,我们需要创建测试存根、伪对象及模拟对象。

这一篇中我们会开始接触这些核心技术,借助存根破除依赖,下一篇我们会使用模拟对象进行交互测试,再下一篇我们则进一步使用隔离框架支持适应未来和可用性的功能。

破除依赖之存根  

为何使用存根?

当我们要测试的对象依赖另一个你无法控制(或者还未实现)的对象,这个对象可能是Web服务、系统时间、线程调度或者很多其他东西。

那么重要的问题来了:你的测试代码不能控制这个依赖的对象向你的代码返回什么值,也不能控制它的行为(例如你想摸你一个异常)。

因此,这种情况下你可以使用存根。

存根的简要介绍

(1)外部依赖项

一个外部依赖项是系统中的一个对象,被测试代码与这个对象发生交互,但你不能控制这个对象。(常见的外部依赖项包括:文件系统、线程、内存以及时间等)

(2)存根

一个存根(Stub)是对系统中存在的一个依赖项(或者协作者)的可控制的替代物。通过使用存根,你在测试代码时无需直接处理这个依赖项。

发现项目中的外部依赖

继续上一篇中的LogAn案例,假设我们的IsValidLogFilename方法会首先读取配置文件,如果配置文件说支持这个扩展名,就返回true:

    public bool IsValidLogFileName(string fileName){// 读取配置文件// 如果配置文件说支持这个扩展名,则返回true}

那么问题来了:一旦测试依赖于文件系统,我们进行的就是集成测试,会带来所有与集成测试相关的问题—运行速度较慢,需要配置,一次测试多个内容等。

换句话说,尽管代码本身的逻辑是完全正确的,但是这种依赖可能导致测试失败。

避免项目中的直接依赖

想要破除直接依赖,可以参考以下两个步骤:

(1)找到被测试对象使用的外部接口或者API;

(2)把这个接口的底层实现替换成你能控制的东西;

对于我们的LogAn项目,我们要做到替代实例不会访问文件系统,这样便破除了文件系统的依赖性。因此,我们可以引入一个间接层来避免对文件系统的直接依赖。访问文件系统的代码被隔离在一个FileExtensionManager类中,这个类之后将会被一个存根类替代,如下图所示:

在上图中,我们引入了存根 ExtensionManagerStub 破除依赖,现在我们得代码不应该知道也不会关心它使用的扩展管理器的内部实现。

重构代码提高可测试性

一个单元测试是一段自动化的代码,这段代码调用被测试的工作单元,之后对这个单元的单个最终结果的某些假设进行检验。

有两类打破依赖的重构方法,二者相互依赖,他们被称为A型和B型重构。

(1)A型 把具体类抽象成接口或委托

下面我们实践抽取接口将底层实现变为可替换的,继续上述的IsValidLogFileName方法。

Step1.我们将和文件系统打交道的代码分离到一个单独的类中,以便将来在代码中替换带对这个类的调用。

① 使用抽取出的类

    public bool IsValidLogFileName(string fileName){FileExtensionManager manager = new FileExtensionManager();return manager.IsValid(fileName);}

② 定义抽取出的类

    public class FileExtensionManager : IExtensionManager{public bool IsValid(string fileName){bool result = false;// 读取文件return result;}}

Step2.然后我们从一个已知的类FileExtensionManager抽取出一个接口IExtensionManager。

    public interface IExtensionManager{bool IsValid(string fileName);}

Step3.创建一个实现IExtensionManager接口的简单存根代码作为可替换的底层实现。

    public class AlwaysValidFakeExtensionManager : IExtensionManager{public bool IsValid(string fileName){return true;}}

于是,IsValidLogFileName方法就可以进行重构了:

    public bool IsValidLogFileName(string fileName){IExtensionManager manager = new FileExtensionManager();return manager.IsValid(fileName);}

但是,这里被测试方法还是对具体类进行直接调用,我们必须想办法让测试方法调用伪对象而不是IExtensionManager的原本实现,于是我们想到了DI(依赖注入),这时就需要B型重构。

(2)B型 重构代码,从而能够对其注入这种委托和接口的伪实现。

刚刚我们想到了依赖注入,依赖注入的主要表现形式就是构造函数注入与属性注入,于是这里我们主要来看看构造函数层次与属性层次如何注入一个伪对象。

① 通过构造函数注入伪对象

根据上图所示的流程,我们可以重构LogAnalyzer代码:

    public class LogAnalyzer{private IExtensionManager manager;public LogAnalyzer(IExtensionManager manager){this.manager = manager;}public bool IsValidLogFileName(string fileName){return manager.IsValid(fileName);}}

其次,再添加新的测试代码:

    [TestFixture]public class LogAnalyzerTests{[Test]public void IsValidFileName_NameSupportExtension_ReturnsTrue(){// 准备一个返回true的存根FakeExtensionManager myFakeManager = new FakeExtensionManager();myFakeManager.WillBeValid = true;// 通过构造器注入传入存根LogAnalyzer analyzer = new LogAnalyzer(myFakeManager);bool result = analyzer.IsValidLogFileName("short.ext");Assert.AreEqual(true, result);}// 定义一个最简单的存根internal class FakeExtensionManager : IExtensionManager{public bool WillBeValid = false;public bool IsValid(string fileName){return WillBeValid;}}}    

Note:这里将伪存根类和测试代码放在一个文件里,因为目前这个伪对象只在这个测试类内部使用。它比起手工实现的伪对象和测试代码放在不同文件中,将它们放在一个文件里的话,定位、阅读以及维护代码都要容易的多。

② 通过属性设置注入伪对象

构造函数注入只是方法之一,属性也经常用来实现依赖注入。

根据上图所示的流程,我们可以重构LogAnalyzer类:

    public class LogAnalyzer{private IExtensionManager manager;// 允许通过属性设置依赖项public IExtensionManager ExtensionManager{get{return manager;}set{manager = value;}}public LogAnalyzer(){this.manager = new FileExtensionManager();}public bool IsValidLogFileName(string fileName){return manager.IsValid(fileName);}}

其次,新增一个测试方法,改为属性注入方式:

    [Test]public void IsValidFileName_SupportExtension_ReturnsTrue()
{// 设置要使用的存根,确保其返回trueFakeExtensionManager myFakeManager = new FakeExtensionManager();myFakeManager.WillBeValid = true;// 创建analyzer,注入存根LogAnalyzer log = new LogAnalyzer();log.ExtensionManager = myFakeManager;bool result = log.IsValidLogFileName("short.ext");Assert.AreEqual(true, result);}

Note : 如果你想表明被测试类的某个依赖项是可选的,或者测试可以放心使用默认创建的这个依赖项实例,这时你就可以使用属性注入。

抽取和重写

抽取和重写是一项强大的技术,可直接替换依赖项,实现起来快速干净,可以让我们编写更少的接口、更多的虚函数。

还是继续上面的例子,首先改造被测试类(位于Edison.LogAn),添加一个返回真实实例的虚工厂方法,正常在代码中使用工厂方法:

    public class LogAnalyzerUsingFactoryMethod{public bool IsValidLogFileName(string fileName){// use virtual methodreturn GetManager().IsValid(fileName);}protected virtual IExtensionManager GetManager(){// hard codereturn new FileExtensionManager();}}

其次,在改造测试项目(位于Edison.LogAn.UnitTests),创建一个新类,声明这个新类继承自被测试类,创建一个我们要替换的接口(IExtensionManager)类型的公共字段(不需要属性get和set方法):

    public class TestableLogAnalyzer : LogAnalyzerUsingFactoryMethod{public IExtensionManager manager;public TestableLogAnalyzer(IExtensionManager manager){this.manager = manager;}// 返回你指定的值protected override IExtensionManager GetManager(){return this.manager;}}

最后,改造测试代码,这里我们创建的是新派生类而非被测试类的实例,配置这个新实例的公共字段,设置成我们在测试中创建的存根实例FakeExtensionManager:

    [Test]public void OverrideTest(){FakeExtensionManager stub = new FakeExtensionManager();stub.WillBeValid = true;// 创建被测试类的派生类的实例TestableLogAnalyzer logan = new TestableLogAnalyzer(stub);bool result = logan.IsValidLogFileName("stubfile.ext");Assert.AreEqual(true, result);}

小结  

本篇我们开始了单元测试核心技术之一存根的学习,通过使用存根可以破除依赖。下一篇我们会使用模拟对象进行交互测试,再下一篇我们则进一步使用隔离框架支持适应未来和可用性的功能。

参考资料  

(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持续集成实践

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

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


点个【在看】如何?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/312395.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vilatile 深入理解java虚拟机_深入理解Java虚拟机(jvm性能调优+内存模型+虚拟机原理)...

本套课程从虚拟机的发展历史,虚拟机的内存结构,对象的分配与回收以及字节码,类加载等多个方面深入地对Java虚拟机进行了剖析。内容详实,语言通俗易懂。理论结合实践,让学习本套视频的朋友可以更快的理解虚拟机的原理&a…

二维费用的背包问题

2020.12.30开始学习AcWing算法《算法竞赛进阶指南》&#xff1b; 上传博客方便复习。 //Wecccccccc //2020.12.31 #include <iostream> using namespace std; int n, v, m, dp[120][120];int main() {cin >> n >> v >> m;for (int i 0; i < n; i) …

UnitTest in .NET(Part 1)

Photo &#xff1a;Unit Test in Visual Studio文 | Edison Zhou2015年看了Roy Osherove的《单元测试的艺术》一书&#xff0c;颇有收获。因此&#xff0c;我在当时就将我的学习笔记过程记录了下来&#xff0c;并分为四个部分分享成文&#xff0c;与各位Share。本篇作为入门&am…

找出一个字符串中出现次数最多的字_Day34:第一个只出现一次的字符

剑指Offer_编程题——第一个只出现一次的字符题目描述&#xff1a;在一个字符串(0<字符串长度<10000&#xff0c;全部由字母组成)中找到第一个只出现一次的字符&#xff0c;并返回它的位置&#xff0c;如果没有则返回-1&#xff0c;需要区分大小写。(从0开始计数)。具体要…

2020 WTM 继续向前

WTM3.1 正式发布在过去的2019年&#xff0c;承蒙各位的厚爱&#xff0c;WTM从零开始一年的时间在GitHub上收获了将近1600星&#xff0c;nuget上的下载量累计超过10万。WTM所坚持的低码开发&#xff0c;快速实现的理念受到了越来越多.netcore使用者的喜爱。在2020年&#xff0c;…

autowired用在static_java – @Autowired和static方法

您可以通过以下解决方案之一来实现此目的&#xff1a;使用构造函数Autowired这种方法将构造需要一些bean作为构造函数参数的bean。在构造函数代码中&#xff0c;设置静态字段的值为参数为构造函数执行。样品&#xff1a;Componentpublic class Boo {private static Foo foo;Aut…

使用ASP.NET Core 3.x 构建 RESTful API - 4.3 HTTP 方法的安全性和幂等性

什么样的HTTP方法是安全的&#xff1f; 如果一个方法不会改变资源的表述&#xff0c;那么这个方法就被认为是安全的。 例如 HTTP GET 和 HTTP HEAD 就被认为是安全的&#xff0c;但需要注意的是&#xff0c;这并不意味着执行GET请求就不会引起其它的资源操作&#xff0c;在表面…

混合背包问题

2020.12.30开始学习AcWing算法《算法竞赛进阶指南》&#xff1b; 上传博客方便复习。 //Wecccccccc //2020.12.31 #include <iostream> using namespace std; #include <vector>struct note {int kind;int v, w; };vector <note> kinds; int n, v, v1, w, dp…

.NET Core 3.1通用主机原理及使用

一、前言只是讲asp.net core 3.x通用主机的大致原理&#xff0c;这些东西是通过查看源码以及自己根据经验总结得来的&#xff0c;在文章中不会深入源码&#xff0c;因为个人觉得懂原理就晓得扩展点&#xff0c;后期碰到有需求的时候再仔细去研究源码也不迟。阅读前你应该先去了…

火焰效果材质实现_「游戏开发」使用Unity实现魔法火焰效果

*本文转载自公众号“Unity官方平台”。本文由视觉效果艺术家Evgeny Starostin分享如何使用Unity制作魔法火焰效果的过程&#xff0c;让我们一起学习和制作魔法火焰吧。下面是魔法火焰效果图。项目下载本文提供项目工程及着色器下载。本文为转载文章&#xff0c;请关注公众号“U…

激光炸弹(二维前缀和问题)

2020.12.30开始学习AcWing算法《算法竞赛进阶指南》&#xff1b; 上传博客方便复习。 //Wecccccccc //2021.1.2 #include <iostream> using namespace std; const int N 5010; int mp[N][N]; int main() {int n,m,k,r,x,y,w;cin>>k>>r;r min(r,5001);n m …

错误使用.Net Redis客户端CSRedisCore,自己挖坑自己填

本文2019年中原创首发于博客园&#xff0c;当时使用CSRedisCore的排障思路引起很大反响&#xff0c;当时被张队公众号翻牌&#xff0c;本次转回公号。背景上次Redis MQ分布式改造之后&#xff0c;编排的容器稳定运行一个多月&#xff0c;昨天突然收到ETL端同事通知&#xff0c;…

mysql 整数_MySQL 整数(int)数据类型

MySQL中定义数据字段的类型对你数据库的优化是非常重要的。MySQL支持多种类型&#xff0c;大致可以分为三类&#xff1a;数值、日期/时间和字符串(字符)类型.数值类型(整型)MySQL支持所有标准SQL数值数据类型。这些类型包括严格数值数据类型(INTEGER、SMALLINT、DECIMAL和NUMER…

最佳牛围栏(二分)

2020.12.30开始学习AcWing算法《算法竞赛进阶指南》&#xff1b; 上传博客方便复习。 #include <iostream> using namespace std; #include <algorithm> const int N 100001; int n, m; int cow[N]; double sum[N];bool check (double ave) {for (int i 1; i <…

IHostingEnvironment VS IHostEnvironment - .NET Core 3.0中的废弃类型

原文&#xff1a;https://andrewlock.net/ihostingenvironment-vs-ihost-environment-obsolete-types-in-net-core-3/作者&#xff1a;Andrew Lock译者&#xff1a;Lamond Lu本篇是如何升级到ASP.NET Core 3.0系列文章的第二篇。Part 1 - 将.NET Standard 2.0 类库转换为.NET C…

mysql stack is full_mysql优化之表的优化与列类型选择

表的优化与列类型选择列选取原则####1、字段类型优先级整型 > date,time > char,varchar > blob因为 整型&#xff0c;time运算快&#xff0c;节省空间&#xff0c;char/varchar要考虑字符集的转换与排序时的校对集&#xff0c;速度慢&#xff0c;blob无法使用内存临时…

给一个不多于5位的正整数,求出它是几位数?

分别求每一位上的数字&#xff0c;关系如下&#xff1a; 求万位上的数&#xff1a;ten_thousandnum/10000; 求千位上的数&#xff1a;thousand(num%10000)/1000; 求百位上的数&#xff1a;hundred(num%1000)/100; 求十位上的数&#xff1a;ten(num%100)/10; 求个位上的数&#…

【敏捷案例】老板太外行,朝令夕改!要不要拿了年终奖就撤?

快到春节了&#xff0c;不知道有多少人在等着春节后跳槽&#xff0c;想跳槽的原因也很多&#xff0c;其中一个比较普遍的原因是和老板聊不到一块儿去。前两天&#xff0c;有个学员咨询了我们一个问题&#xff0c;因为这个问题比较普遍&#xff0c;拿出来和大家一起讨论一下~提出…

mysql商品规格设计_[电商]我的商品规格设计思路

这部分的设计必然少不了会有商品、货品、规格、规格值表。先说下我对四个表之间关系设计&#xff1a;商品与货品是一对多的关系。规格与规格值是一对多的关系。就这两种关系&#xff0c;关系很清晰&#xff0c;其实我刚开始的设计是这样的&#xff1a;商品与货品是一对多的关系…

冒泡排序及简单优化

普通冒泡排序&#xff1a; //Wecccccccc //2021.1.6 #include <iostream> using namespace std;int main() {int n, a[100];cin >> n;for (int i 0; i < n; i) {cin >> a[i];}for (int i 0; i < n - 1; i) {for (int j 0; j < n - 1; j) {if (a…