UnitTest in .NET(Part 1)


Photo :Unit Test in Visual Studio

文 | Edison Zhou

2015年看了Roy Osherove的《单元测试的艺术》一书,颇有收获。因此,我在当时就将我的学习笔记过程记录了下来,并分为四个部分分享成文,与各位Share。本篇作为入门,介绍了单元测试的基础知识,例如:如何使用一个测试框架,基本的自动化测试属性等等,还有对应的三种测试类型。相信你可以对编写单元测试从一无所知到及格水平,这也是原书作者的目标。

1  单元测试基础   

什么是单元测试?

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

单元测试几乎都是用单元测试框架编写的。单元测试容易编写,能够快速运行。单元测试可靠、可读,并且可维护。

只要产品代码不发生变化,单元测试的结果是稳定的。

与集成测试有何区别?

Note:集成测试是对一个工作单元进行的测试,这个测试对被测试的工作单元没有完全的控制,并使用该单元的一个或多个真实依赖物,例如时间、网络、数据库、线程或随机数产生器等。

总的来说,集成测试会使用真实依赖物,而单元测试则把被测试单元和其依赖物隔离开,以保证单元测试结果高度稳定,还可以轻易控制和模拟被测试单元行为的任何方面。                                  

2  测试驱动开发基础   

传统的单元测试流程

测试驱动开发流程概要

如上图所示,TDD和传统开发方式不同,我们首先会编写一个会失败的测试,然后创建产品代码,并确保这个测试通过,接下来就是重构代码或者创建另一个会失败的测试。

在前年底(2018)加入X公司的时候,我的老板曾经推行了TDD给我们,我也弄了一些TDD的demo应用,不过最终还是没有推行起来,源自于项目的交付压力,只保留了事后写单元测试的要求(而且还是Service类的核心主流程业务代码的单元测试)作为保护层,在增加功能修复Bug时需要通过单元测试才能提交代码。

3  第一个单元测试   

NUnit

NUnit 是从流行的Java单元测试框架JUnit直接移植过来的,之后NUnit在设计和可用性上做了极大地改进,和JUnit有了很大的区别,给日新月异的测试框架生态系统注入了新的活力。

作为一名.NET程序员,如何在VS中安装NUnit并能够在VS中直接运行测试呢?

Step1.在NuGet中找到NUnit并安装

Step2.在NuGet中找到NUnit Test Adapter并安装

LogAn示例项目

LogAn (Log And Notificaition)

场景:公司有很多内部产品,用于在客户场地监控公司的应用程序。所有这些监控产品都会写日志文件,日志文件存放在一个特定的目录中。日志文件的格式是你们公司自己制定的,无法用现有的第三方软件进行解析。你的任务是:实现一个产品,对这些日志文件进行分析,在其中搜索特定的情况和事件,这个产品就是LogAn。找到特定的情况和事件后,这个产品应该通知相关的人员。

在本次的单元测试实践中,我们会一步一步编写测试来验证LogAn的解析、事件识别以及通知功能。首先,我们需要了解使用NUnit来编写单元测试(当然你也可以使用MsTest也是可以的)。

编写第一个单元测试

(1)我们的测试从以下这个LogAnalyzer类开始,这个类暂时只有一个方法IsValidLogFileName:

    public class LogAnalyzer{public bool IsValidLogFileName(string fileName){if (fileName.EndsWith(".SLF")){return false;}return true;}}

这个方法检查文件扩展名,据此判断一个文件是不是有效的日志文件。

这里在if中故意去掉了一个!运算符,因此这个方法就包含了一个Bug-当文件名以.SLF结尾时会返回false,而不是返回true。这样,我们就能看到测试失败时在测试运行期中显示什么内容。

(2)新建一个类库项目,命名为Manulife.LogAn.UnitTests(被测试项目项目名为Manulife.LogAn.Lib)。添加一个类,取名为LogAnalyzerTests.cs。

(3)在LogAnalyzerTests类中新增一个测试方法,取名为IsValidFileName_BadExtension_ReturnsFalse()。

首先,我们要明确如何编写测试代码,一般来说,一个单元测试通常包含三个行为:

因此,根据以上三个行为,我们可以编写出以下的测试方法:(其中断言部分使用了NUnit框架提供的Assert类)

    
    [TestFixture]public class LogAnalyzerTests{[Test]public void IsValidFileName_BadExtension_ReturnsFalse(){LogAnalyzer analyzer = new LogAnalyzer();bool result = analyzer.IsValidLogFileName("filewithbadextension.foo");Assert.AreEqual(false, result);}}

其中,属性[TestFixture]和[Test]是NUnit的特有属性,NUnit用属性机制来识别和加载测试。这些属性就像一本书里的书签,帮助测试框架识别记载程序集里面的重要部分,以及哪些部分是需要调用的测试。

1.[TestFixture]加载一个类上,标识这个类是一个包含自动化NUnit测试的类;

2.[Test]加在一个方法上,标识这个方法是一个需要调用的自动化测试;

另外,再说一下测试方法名称的规范,一般包含三个部分:[UnitOfWorkName]_[ScenarioUnderTest]_[ExpectedBehavior]

1.UnitOfWorkName  被测试的方法、一组方法或者一组类

2.Scenario  测试进行的假设条件,例如“登入失败”,“无效用户”或“密码正确”等

3.ExpectedBehavior  在测试场景指定的条件下,你对被测试方法行为的预期  

奔跑吧,第一个单元测试

(1)编写好测试代码之后,点击"测试"->"运行"->"所有测试"

(2)然后,点击"测试"->"窗口"->"测试窗口管理器",你会看到以下场景

从上图可以看出,我们得测试方法并没有通过,我们期望(Expected)的结果是False,而实际(Actual)的结果却是True。

继续添加你的单元测试

(1)通常在进行单元测试时我们会考虑到代码覆盖率,点击"测试"->"分析代码覆盖率"->"所有测试",你可以看到以下结果:80%

(2)这时,我们需要想出完善的测试策略来覆盖所有的情况,因此我们添加一些测试方法来提高我们的代码覆盖率。这里我们添加两个方法,一个测试大写文件扩展名,一个测试小写文件扩展名:

    
    [Test]public void IsValidFileName_GoodExtensionLowercase_ReturnsTrue(){LogAnalyzer analyzer = new LogAnalyzer();bool result = analyzer.IsValidLogFileName("filewithgoodextension.slf");Assert.AreEqual(true, result);}[Test]public void IsValidFileName_GoodExtensionUppercase_ReturnsTrue(){LogAnalyzer analyzer = new LogAnalyzer();bool result = analyzer.IsValidLogFileName("filewithgoodextension.SLF");Assert.AreEqual(true, result);}

这时测试结果如下图所示:

这时再来看看代码覆盖率:100%

(3)为了让所有的测试都能通过,这时我们需要修改源代码,改用大小写不敏感的字符串匹配:

    public bool IsValidLogFileName(string fileName){if (!fileName.EndsWith(".SLF", StringComparison.CurrentCultureIgnoreCase)){return false;}return true;}

这时,我们再来运行一下所有的测试(也可以选择 运行未通过的测试)来看下由红到绿的快感。单元测试的理念很简单:只有所有的测试都通过,继续前行的绿灯才会亮起。哪怕只有一个测试失败了,进度条上都会亮起红灯,显示你的系统(或者测试)出现了问题

4  More NUnit

参数化重构单元测试

NUnit中有个叫做 参数化测试(Parameterized Tests)的功能,我们可以借助[TestCase]标签特性来重构我们的单元测试:

    
    [TestCase("filewithgoodextension.slf")][TestCase("filewithgoodextension.SLF")]public void IsValidFileName_ValidExtensions_ReturnsTrue(string fileName){LogAnalyzer analyzer = new LogAnalyzer();bool result = analyzer.IsValidLogFileName(fileName);Assert.AreEqual(true, result);}

可以看到,借助TestCase特性,测试数目没有改变,但是测试代码却变得更易维护,更加易读。

SetUp与TearDown

NUnit还有一些特别的标签特性,可以很方便地控制测试前后的设置和清理状态工作,他们就是[SetUp]和[TearDown]。

1.[SetUp] 这个标签加在一个方法上,NUnit每次在运行测试类里的任何一个测试时都会先运行这个setup方法;

2.[TearDown] 这个标签标识一个方法应该在测试类里的每个测试运行完成之后执行;

    
    [TestFixture]public class LogAnalyzerTests{private LogAnalyzer analyzer = null;[SetUp]public void Setup(){analyzer = new LogAnalyzer();}[Test]public void IsValidFileName_ValidFileLowerCased_ReturnsTrue(){bool result = analyzer.IsValidLogFileName("whatever.slf");Assert.IsTrue(result, "filename should be valid!");}[Test]public void IsValidFileName_ValidFileUpperCased_ReturnsTrue(){bool result = analyzer.IsValidLogFileName("whatever.SLF");Assert.IsTrue(result, "filename should be valid!");}[TearDown]public void TearDown(){analyzer = null;}}

我们可以把setup和teardown方法想象成测试类中测试的构造函数和析构函数,在每个测试类中只能有一个setup和teardown方法,这两个方法对测试类中的每个方法只执行一次。

不过,使用[Setup]越多,测试代码可读性就越差。原书作者推荐采用工厂方法(Factory Method)初始化被测试的实例。

    /// <summary>/// 工厂方法初始化 LogAnalyzer/// 既节省编写代码的时间,又使每个测试内的代码更简洁易读/// 同时保证 LogAnalyzer 总是用同样的方式初始化/// </summary>private static LogAnalyzer MakeAnalyzer(){return new LogAnalyzer();}

在测试方法中可以直接使用:

    
    [Test]public void IsValidFileName_BadExtension_ReturnsFalse(){LogAnalyzer analyzer = MakeAnalyzer();bool result = analyzer.IsValidLogFileName("filewithbadextension.foo");Assert.AreEqual(false, result);}

检验预期的异常

很多时候,我们的方法中会抛出一些异常,这时如果我们的测试也应该做一些修改。在NUnit中,提供了一个API : Assert.Catch<T>(delegate)

首先,我们修改一下被测试的方法,增加一行判断文件名是否为空的代码:

    public bool IsValidLogFileName(string fileName){if(string.IsNullOrEmpty(fileName)){throw new ArgumentException("filename has to be provided");}if (!fileName.EndsWith(".SLF", StringComparison.CurrentCultureIgnoreCase)){return false;}return true;}

然后,我们新增一个测试方法,使用Assert.Catch来检测异常是否一致:

   [Test]public void IsValidFileName_EmptyName_Throws(){LogAnalyzer analyzer = new LogAnalyzer();// 使用Assert.Catchvar ex = Assert.Catch<Exception>(() => analyzer.IsValidLogFileName(string.Empty));// 使用Assert.Catch返回的Exception对象StringAssert.Contains("filename has to be provided", ex.Message);}

忽略测试

有时候测试代码有问题,但是我们又需要把代码签入到主代码树中。在这种罕见的情况下(虽然确实非常少),可以给那些测试代码自身有问题的测试加一个[Ignore]标签特性。

    [Test][Ignore("there is a problem with this test!")]public void IsValidFileName_ValidFile_ReturnsTrue(){// ...}

可以看到,这个测试确实被忽略了:

设置测试的类别

我们可以把测试按照指定的测试类别运行,使用[Category]标签特性就可以实现这个功能:

    [Test][Category("Fast Tests")]public void IsValidFileName_BadExtension_ReturnsFalse(){LogAnalyzer analyzer = new LogAnalyzer();bool result = analyzer.IsValidLogFileName("filewithbadextension.foo");Assert.AreEqual(false, result);}

测试系统状态的改变

此前我们得测试都有返回值,而很多要测试的方法都没有返回值,而只是改变对象中的某些状态,我们又该如何测试呢?

首先,我们修改IsValidLogFileName方法,增加一个状态属性:

    public class LogAnalyzer{public bool WasLastFileNameValid { get; set; }public bool IsValidLogFileName(string fileName){// 改变系统状态WasLastFileNameValid = false;if(string.IsNullOrEmpty(fileName)){throw new ArgumentException("filename has to be provided");}if (!fileName.EndsWith(".SLF", StringComparison.CurrentCultureIgnoreCase)){return false;}// 改变系统状态WasLastFileNameValid = true;return true;}}

其次,我们编写一个测试,对系统状态进行断言:

    
    [TestCase("badfile.foo", false)][TestCase("goodfile.slf", true)]public void IsValidFileName_WhenCalled_ChangesWasLastFileNameValid(string fileName, bool expected){LogAnalyzer analyzer = new LogAnalyzer();analyzer.IsValidLogFileName(fileName);Assert.AreEqual(expected, analyzer.WasLastFileNameValid);}

5  小结

这一篇作为入门,带领大家领略了一下单元测试的概念,如何编写单元测试,如何在VS中应用NUnit进行单元测试。相信大家以前都用过MSTest,而我们这里却使用了NUnit。所以,下面我们来总结一下MSTest与NUnit在特性标签上的一些区别:

MSTest AttributeNUnit Attribute用途
[TestClass][TestFixture]定义一个测试类,里面可以包含很多测试函数和初始化、销毁函数(以下所有标签和其他断言)。
[TestMethod][Test]定义一个独立的测试函数。
[ClassInitialize][TestFixtureSetUp]定义一个测试类初始化函数,每当运行测试类中的一个或多个测试函数时,这个函数将会在测试函数被调用前被调用一次(在第一个测试函数运行前会被调用)。
[ClassCleanup][TestFixtureTearDown]定义一个测试类销毁函数,每当测试类中的选中的测试函数全部运行结束后运行(在最后一个测试函数运行结束后运行)。
[TestInitialize][SetUp]定义测试函数初始化函数,每个测试函数运行前都会被调用一次。
[TestCleanup][TearDown]定义测试函数销毁函数,每个测试函数执行完后都会被调用一次。
[AssemblyInitialize]--定义测试Assembly初始化函数,每当这个Assembly中的有测试函数被运行前,会被调用一次(在Assembly中第一个测试函数运行前会被调用)。
[AssemblyCleanup]--定义测试Assembly销毁函数,当Assembly中所有测试函数运行结束后,运行一次。(在Assembly中所有测试函数运行结束后被调用)
[DescriptionAttribute][Category]定义标识分组。

参考资料

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

(2)Aileer,《对比MS Test与NUnit Test框架》

2020后记:虽然这是一篇发表于2015年的文章,但我至今觉得仍有价值。因为我发现在.NET圈,还是有很多童鞋不了解单元测试和不喜欢写单元测试,不懂其价值就不会形成增强回路。所谓增强回路,就是我单元测试写的越多,以后修改代码增加功能就不容易出现Bug(这里主要指集成测试阶段、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/312392.shtml

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

相关文章

找出一个字符串中出现次数最多的字_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;…

使用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;…

最佳牛围栏(二分)

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…

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

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

【2019总结篇】谈谈数字化时代,ERP如何坐稳数字化底座

源宝导读&#xff1a;面向未来&#xff0c;信息化、在线化、智能化&#xff0c;开放应该是数字化转型的核心要素&#xff01;本文将重点介绍2019年明源云ERP开放平台在推进数字化转型中的核心技术层面的实践成果。一、前言面向未来&#xff0c;信息化、在线化、智能化&#xff…

dotNetCore操作Redis(含CentOS7哨兵模式部署)

现在说到使用缓存中间件基本就是 Redis 了&#xff0c;通常开发环境或测试环境部署一个单机版就可以运行了&#xff0c;但要上生产环境还需要进行高可用的方式来部署&#xff0c;本文说说在 CentOS7 中 Redis 高可用的部署以及在 dotNetCore 中怎样调用。环境CentOS&#xff1a…

php和mysql一键安装包_iis+php+mysql一键安装教程和安装包

导读&#xff1a;iis上mysqlphp一键安装 很多用vps的朋友&#xff0c;在windons系统iis上配置mysqlphp环境的时候都非常的头痛&#xff0c;过程非常复杂和麻烦&#xff0c;所以我们推荐大家用mysqlphp一键安装包&#xff1a; 1、下载mysqlphp一键安装包&#xff0c;安装包下载地…

程序员过关斩将--自定义线程池来实现文档转码

背景我司在很久之前&#xff0c;一位很久之前的同事写过一个文档转图片的服务&#xff0c;具体业务如下&#xff1a;1. 用户在客户端上传文档&#xff0c;可以是ppt&#xff0c;word&#xff0c;pdf 等格式&#xff0c;用户上传完成可以在客户端预览上传的文档&#xff0c;预览…

UnitTest in .NET(Part 4)

Photo &#xff1a;Unit Test in Visual Studio文 | Edison Zhou上一篇我们学习了如何使用模拟对象进行交互测试。这一篇我们则会进一步使用隔离框架支持适应未来和可用性的功能。为何使用模拟框架&#xff1f; 对于复杂的交互场景&#xff0c;可能手工编写模拟对象和存根就会变…

Xamarin.Forms弹出对话框插件

微信公众号&#xff1a;Dotnet9&#xff0c;网站&#xff1a;Dotnet9&#xff0c;问题或建议&#xff0c;请网站留言&#xff1b;如果您觉得Dotnet9对您有帮助&#xff0c;欢迎赞赏。Dotnet9.com内容目录实现效果业务场景编码实现本文参考源码下载1.实现效果弹出动画 2.业务场景…

Pycharm安装第三方库

转载地址&#xff1a; https://www.cnblogs.com/bwjblogs/p/12839463.html 今晚想安装一些第三方库但是pip版本低&#xff0c;安装一直报错&#xff0c;输入升级的命令也一直不行。于是在pycharm上安装&#xff0c;但是还是一直失败&#xff0c;下面提出解决办法。 然后在搜索…

CAP 3.0 版本正式发布

前言大家好&#xff0c;我们很高兴宣布 CAP 发布了 3.0 版本正式版。自从上次 CAP 2.6 版本发布 以来&#xff0c;已经过去了几个月的时间&#xff0c;关注的朋友可能知道&#xff0c;在这几个月的时间里&#xff0c;也发布了几个预览版的 3.0 版本的NuGet包。3.0 是一个主要版…

mysql字符集变为gbk_MYSQL数据库默认latin1字符集转换为GBK或UTF8

可以采用下面的方法latin1字符集转换为gbk字符集或utf8字符集。具体的转换步骤如下&#xff1a;一、latin1转gbk1、导出数据库mysqldump --default-character-setlatin1 -h 数据库连接ip -u root -P 3306 -p数据库密码 db_name table_name > /usr/home/test/table_name.sql2…