- A+
1. 为什么需要单元测试
在我们之前,测试某些功能是否能够正常运行时,我们都将代码写到Main方法中,当我们测试第二个功能时,我们只能选择将之前的代码清掉,重新编写。此时,如果你还想重新测试你之前的功能时,这时你就显得有些难为情了,因为代码都被你清掉了。当然你完全可以把代码写到一个记事本中进行记录,但是这样总归没有那么方便。当然你也可以重新新建一个项目来测试新的功能,但随着功能越来越多,重新新建项目使得项目越来越多,变得不易维护,此时你若选择使用单元测试功能,就可以完美解决你的困扰。
NUnit 提供了单元测试能力,也是目前用的比较流行的单元测试组件。
2. 什么是NUnit?
官网: NUnit.org
NUnit 是适用于所有 .Net 语言的单元测试框架。最初从 JUnit 移植,当前的生产版本 3 已完全重写,具有许多新功能和对各种 .NET 平台的支持。
NUnit 项目是 .NET 基金会的成员。NUnit由核心团队Rob Prouse,Charlie Poole,Terje Sandstrom,Joseph Musser和Mikkel Nylander Bundgaard运营。.NET 基金会将提供指导和支持,以帮助确保项目的未来。
NUnit的成功是通过我们许多贡献者和团队成员的辛勤工作实现的。核心团队感谢每个人的帮助和贡献,使NUnit取得了成功。据最新统计,各种NUnit软件包在 NuGet.org 上通过了1.26亿次下载。如果没有许多志愿者的奉献精神,我们就无法做到这一点,他们为该项目贡献了自己的时间和知识。
3. NUnit V3 使用
-
通过Nuget 安装 如下三个包
-
NUnit
-
Microsoft.NET.Test.Sdk
- NUnit3TestAdapter
-
NUnit 包应由每个测试程序集引用,但不应由任何其他测试程序集引用。
NUnit使用自定义特性来标识测试。 所有的NUnit属性都包含在NUnit中的 框架的名称空间。 包含测试的每个源文件必须包含该名称空间的using语句,项目必须引用框架程序集nunit.framework.dll。
4. 特性
Attribute | Usage |
---|---|
Apartment Attribute | 指示测试应在特定单元中运行。 |
Author Attribute | 提供测试作者的姓名。 |
Category Attribute | 为测试指定一个或多个类别。 |
Combinatorial Attribute | 为提供的值的所有可能组合生成测试用例。 |
Culture Attribute | 指定应为其运行测试或夹具的区域性。 |
Datapoint Attribute | 提供数据理论. |
DatapointSource Attribute | 提供数据源理论. |
DefaultFloatingPointTolerance Attribute | 指示测试应使用指定的容差作为浮点数和双重比较的默认值。 |
Description Attribute | 将描述性文本应用于测试、测试修复或程序集。 |
Explicit Attribute | 指示除非显式运行,否则应跳过测试。 |
FixtureLifeCycle Attribute | 指定夹具的生命周期,允许为每个测试用例构造测试夹具的新实例。在测试用例并行性很重要的情况下很有用。 |
Ignore Attribute | 指示由于某种原因不应运行测试。 |
LevelOfParallelism Attribute | 指定程序集级别的并行度级别。 |
MaxTime Attribute | 指定测试用例成功的最长时间(以毫秒为单位)。 |
NonParallelizable Attribute | 指定测试及其后代不能并行运行。 |
NonTestAssembly Attribute | 指定程序集引用 NUnit 框架,但不包含测试。 |
OneTimeSetUp Attribute | 标识在任何子测试之前要调用一次的方法。 |
OneTimeTearDown Attribute | 标识在所有子测试之后要调用一次的方法。 |
Order Attribute | 指定在包含的夹具或套件中运行装饰测试的顺序。 |
Pairwise Attribute | 为提供的所有可能值对生成测试用例。 |
Parallelizable Attribute | 指示测试和/或其后代是否可以并行运行。 |
Platform Attribute | 指定应为其运行测试或夹具的平台。 |
Property Attribute | 允许在任何测试用例或夹具上设置命名属性。 |
Random Attribute | 指定生成随机值作为参数化测试的参数。 |
Range Attribute | 指定一系列值作为参数化测试的参数。 |
Repeat Attribute | 指定应多次执行修饰方法。 |
RequiresThread Attribute | 指示测试方法、类或程序集应在单独的线程上运行。 |
Retry Attribute | 如果测试失败,则导致重新运行测试,最多可达最大次数。 |
Sequential Attribute | 使用按提供的顺序排列的值生成测试用例,无需其他组合。 |
SetCulture Attribute | 设置测试持续时间内的当前区域性。 |
SetUICulture Attribute | 设置测试持续时间内的当前 UI 区域性。 |
SetUp Attribute | 指示在每个测试方法之前调用的 TestFixture 方法。 |
SetUpFixture Attribute | 使用命名空间中所有测试夹具的一次性设置或拆卸方法标记类。 |
SingleThreaded Attribute | 标记要求其所有测试在同一线程上运行的夹具。 |
TearDown Attribute | 指示在每个测试方法之后调用的 TestFixture 方法。 |
Test Attribute | 标记表示测试的 TestFixture 的方法。 |
TestCase Attribute | 将带有参数的方法标记为测试,并提供内联参数。 |
TestCaseSource Attribute | 将带有参数的方法标记为测试,并提供参数源。 |
TestFixture Attribute | 将类标记为测试夹具,并可能提供内联构造函数参数。 |
TestFixtureSetup Attribute | 已弃用,同义词: OneTimeSetUp Attribute. |
TestFixtureSource Attribute | 将类标记为测试夹具,并为构造函数参数提供源。 |
TestFixtureTeardown Attribute | 已弃用,同义词: OneTimeTearDown Attribute. |
TestOf Attribute | 指示要测试的类的名称或类型。 |
Theory Attribute | 将测试方法标记为理论,这是NUnit中的一种特殊测试。 |
Timeout Attribute | 为测试用例提供超时值(以毫秒为单位)。 |
Values Attribute | 为测试方法的参数提供一组内联值。 |
ValueSource Attribute | 为测试方法的参数提供值的源 |
5. TestFixture
此属性标记包含测试以及(可选)设置或拆解方法的类。
现在,对用作测试夹具的类的大多数限制都已消除。TestFixture类:
-
可以是公共的、受保护的、私有的或内部的。
-
可能是静态类。
-
可以是泛型的,只要提供了任何类型参数,或者可以从实际参数中推断出来。
-
可能不是抽象的 - 尽管该属性可以应用于旨在用作TestFixture基类的抽象类。
-
如果 TestFixtureAttribute 中没有提供任何参数,则该类必须具有默认构造函数。
-
如果提供了参数,则它们必须与其中一个构造函数匹配。
如果违反了这些限制中的任何一个,则该类不可作为测试运行,并且将显示为错误。
建议构造函数没有任何副作用,因为 NUnit 可能会在会话过程中多次构造对象。
从 NUnit 2.5 开始,TestFixture 属性对于非参数化、非通用Fixture是可选的。只要该类包含至少一个标有 Test、TestCase 或 TestCaseSource 属性的方法,它就会被视为测试夹具。
using NUnit.Framework; namespace MyTest; // [TestFixture] // 2.5 版本以后,可选 public class FirstTest { [Test] public void Test1() { Console.WriteLine("test1,hello"); } }
6. SetUp 设置
此属性在TestFixture内部使用,以提供在调用每个测试方法之前执行的一组通用函数。
SetUp 方法可以是静态方法,也可以是实例方法,您可以在夹具中定义多个方法。通常,多个 SetUp 方法仅在继承层次结构的不同级别定义,如下所述。
如果 SetUp 方法失败或引发异常,则不会执行测试,并报告失败或错误。
using NUnit.Framework; namespace MyTest; // [TestFixture] // 2.5 版本以后,可选 public class FirstTest { [SetUp] public void Init() { Console.WriteLine("init,初始了一些数据"); } private int a = 10; [OneTimeSetUp] // 只执行一次 public void OneTime() { a++; Console.WriteLine("我只执行一次"); } [Test] public void Test1() { Console.WriteLine("test1,hello"); Console.WriteLine($"a的值是:{a}"); } }
输出结果:
init,初始了一些数据 test1,hello
继承
SetUp 属性继承自任何基类。因此,如果基类定义了 SetUp 方法,则会在派生类中的每个测试方法之前调用该方法。
您可以在基类中定义一个 SetUp 方法,在派生类中定义另一个方法。NUnit 将在派生类中调用基类 SetUp 方法之前调用基类 SetUp 方法。
警告
如果在派生类中重写了基类 SetUp 方法,则 NUnit 将不会调用基类 SetUp 方法;NUnit 预计不会使用包括隐藏基方法在内的用法。请注意,每种方法可能都有不同的名称;只要两者都存在属性,每个属性都将以正确的顺序调用。
[SetUp]
笔记
-
尽管可以在同一类中定义多个 SetUp 方法,但您很少应该这样做。与在继承层次结构中的单独类中定义的方法不同,不能保证它们的执行顺序。
-
在 .NET 4.0 或更高版本下运行时,如有必要,可以指定异步方法(c# 中的关键字)。
7. 断言
断言是任何 xUnit 框架中单元测试的核心,NUnit 也不例外。NUnit 提供一组丰富的断言作为 Assert 类的静态方法。
如果断言失败,则不返回方法调用并报告错误。如果测试包含多个断言,则不会执行失败的断言后面的任何断言。因此,通常最好尝试每个测试一个断言。
每个方法都可以在没有消息的情况下调用,使用简单的文本消息或消息和参数。在最后一种情况下,使用提供的文本和参数设置消息的格式。
如果确实存在等效项,则这两种方法将始终给出相同的结果,因为经典方法的方法都是使用约束在内部实现的。例如。。。
Assert.AreEqual(4, 2+2); Assert.That(2+2, Is.EqualTo(4));
Assert.True()
Assert.True用于断言布尔参数是否为true Assert.True的重载方法还支持可空布尔参数
Assert.IsTrue
此断言方法为Assert.True的亲兄弟,二者功能一模一样.
Assert.False
与Assert.True断言状态相反,断言某一参数的结果为false 这里需要特别说明的是,单元测试应该力求简单,明了,断言尤其如此.
Assert.Null
用于断言一个变量是否为null,这里不再举例,但是实际中用的却比较多.
Assert.NotNull
用于断言一个变量不是null,它和Assert.Null()功能相同,只是断言的状态相反.
Nunit里还有其它的前缀有Not的方法,它和不带Not的方法用法一样,只是断言的状态相反
Assert.Throws 用于断言特定方法在运行的时候会抛出异常.此方法有泛型版本,异步版本,这里仅对异步版本进行说明 由于示例越来越复制,我们不能只在测试方法内写一些简单代码进行测试了,这里我们新建一个Person类如下
这个类里面包含一个WhetherNameContainsB方法,用于判断实例的Name是否包含字母B, 这个方法里面有三个逻辑分支,单元测试的时候每一个都要覆盖到,这里我们断言如果name为null则抛出ArgumentNullException 我们编写如下单元测试方法
运行这个测试,则会返回成功状态,因为预期的异常出现了.
Assert.IsEmpty
用于断言字段串是否为空字符串.
Assert.Positive
用于断言数字类型(int,long,float,double,decimal等)为正数(大于零的数)
其实很多断言都可以断言都可以用Assert.True来完成,比如断言一个数是否为正数,可以用Assert.True(a>0),这里由于a只是一个普通变量,使用a>0作为条件主义仍然十分清析,然而到了后面有我们不仅要判断一个变通变量,还要判断lambda表达式,如果条件过于复杂,则语义会变得不是特别清析了,使用Assert自带的静态方法主义会更加清析,可读性更高.
Assert.Negative
用于断言数字类型为负数(小于零,不包括零)
Assert.Zero
用于断言数字类型为数字零
Assert.NotZero
用于断言数字类型不是零.
很多时候,Not包含的范围非常广,进行单元测试是为了在开发阶段找出问题,解决问题,因此断言的范围越窄越好,我们不能仅仅让单元测试通过了事. 比如一个方法返回的结果是数字类型,我们要断定它是正数?大于某一个数的正数?在一定范围的正数?是一个具体的正数?而不能简单的是零,不是零.当然这还要根据业务本身来确实,有些时候范围可能确实很大,但是一定要注意单元测试原则.
Assert.Greater(OrEqual)
用于断言数字类型的变量大于(或者等于)某一个值
Assert.Less(OrEqual)
用于断言数字类型小于(或者等于)某一值
Assert.Contains
用于断言集合中是否包含某一元素. 比如以下方法,用于断言字符串数组中是否包含特定字符串
Assert.AreSame
用于断言两个对象是否相等
这个静态方法并没有提供重载参数用于指定一个比较器来比较引用对象的相等性,需要实现equals和gethashcode方法才能得到预期结果,但在实际中我们往往把比较器放在类外边,如何在比较引用对象的时候加载一个比较器在后面章节会有介绍,这里先略过.
8. Nunit测试基础之复杂断言
上面一篇我们讲解了一些基本断言,利用这些断言我们就可以进行单元测试了,然而仅仅使用简单断言还是不够的,如果逻辑复杂度较高,使用简单的断言会导致单元测试代码量增加,最终导致单元测试本身过于复杂和难以维护.需要说明的是这里所说的复杂断言仍然在Assert的静态方法里面,本身也不是特别复杂,只是比前面讲的秒复杂一些,只是如果没有了这些方法,一些特殊功能实现起来比较费劲基本无法实现.
下面就介绍一下这些方法.
Assert.Catch
Assert.Catch有泛型和异步方法,这里只介绍其泛型方法.很多即使经常使用单元测试功能的人也未必用过这个方法. 其实这个方法和Assert.Throw用法上类似,只是有一点不同的是要测试的方法里的异常可以是catch到的异常的子类,实际开发中,如果我们能确立异常的类型,则最好捕获具体类型异常,然而不能排除有一些不够规范的代码整段代码被一个try catch包围,这时候不一定能够捕获到想要的特定异常,这时候可以使用Assert.Catch
以上代码类似上一节中讲throw时使用的代码,只是这里泛型参数里是Exception而不是具体的异常信息,我们运行这段代码,依然能够测试通过. 在单元测试中,期待的状态越具体越好,然而由于种种原因(比如立项时候没有对代码规范做过多要求,开发者水平不高,要测试的代码是别人写的,写单元测试的人对其中逻辑并不是特别清楚等)我们无法做到非常具体,这个时候可以把要获得的状态放宽以后,待条件完备了再修改单元测试以进一步收窄状态.
Assert.Ignore
Assert.Ignore和Ignore注解功能类似,可以在测试的时候忽略一个单元测试.有些情况下我们需要暂时忽略一个测试,比如说要进行测试的内容有一个外部依赖,现在外部依赖暂时不可用,如果我们不忽略的话测试将会失败,在自动化环境下,失败将导致无法进行下一步动作,此时我们可以暂时忽略这个测试. 忽略的测试前面有一个 黄色叹号标志,警示我们需要注意.
Assert. Fail
我们先看一下面一段代码
在这个单元测试本身使用到了try catch,我们知道WhetherNameContainsB方法在Person类的Name没有提供值的情况下会抛出异常,然而我们的代码并没有断言这个异常存在,此时由于catch代码块存在,会把异常吞掉,因此最终我们断言person的Age为正数的时候将会通过(我们在构造类的时候设置了Age为32) 这显然不行的,这时候我把们Assert.Fail(e.Message)取消注释,测试便会变成失败状态.
Assert.IsNaN
用于断言一个Double类型数字是否是NaN
虽然实际业务中我们并不会写以上代码,但是如果除数和被除数是通过复杂计算得来的则有可能除数和被除数都是零.
Assert.IsInstanceOf
用于断言一个对象是否是指定类型的实例,
如上psn是Person类的一个实例,而Person继承自Object,因此psn也是Object类的实例
Assert.IsAssignableFrom
此方法和以上方法作用相反,它用来断言指定类型是当前对象类型的子类.(Assert.IsInstanceOf判断的是当前对象是指定类型的子类) 这个方法语义不是很明确,很容易搞晕,使用的时候需要特别注意
Assert.Warn
用于使一个测试通过,但是出现警示信息.
配套视频链接:
C# 高级编程,.Net6 系列 开发第三阶段,学完拿捏你的面试官,.net6 进阶学习(已完结)_哔哩哔哩_bilibili