注:本文示例环境
VS2017
XUnit 2.2.0 单元测试框架
xunit.runner.visualstudio 2.2.0 测试运行工具
Moq 4.7.10 模拟框架
什么是单元测试?
确保软件应用程序按作者的期望执行操作,其中最好的一种方法是拥有自动化测试套件。 可以对软件应用程序进行各种不同的测试,包括集成测试、Web 测试、负载测试等。 测试各个软件组件或方法的单元测试是最低级测试。
所谓单元测试(unit testing),就是开发者编写的一小段代码,用于对软件中的最小单元进行检查和验证,其一般验证对象是一个函数或者一个类。通常而言,一个单元测试是用于判断某个特定条件(或者场景)下某个特定函数的行为。
为什么要使用单元测试?
- 大大节约了测试和修改的时间,有效且便于测试各种情况。
- 能快速定位bug(每一个测试用例都是具有针对性)。
- 能使开发人员重新审视需求和功能的设计(难以单元测试的代码,就需要重新设计)。
- 强迫开发者以调用者而不是实现者的角度来设计代码,利于代码之间的解耦。
- 自动化的单元测试能保证回归测试的有效执行。
- 使代码可以放心修改和重构。
- 测试用例,可作为开发文档使用(测试即文档)。
- 测试用例永久保存,支持随时测试。
对于我个人来说,主要是防止自己犯低级错误的,同时也方便修改(BUG修复)而不引入新的问题。可以放心大胆的重构。简言之,这个简单有效的技术就是为了令代码变得更加完美。
既然单元测试有这些好处,为什么我们不去用呢?
可以归纳为以下几个理由。
对单元测试存在的误解,如:单元测试属于测试工作,应该由测试人员来完成,所以单元测试不属于开发人员的职责范围。
答:虽然单元测试虽然叫做"测试",但实际属于开发范畴,应该由开发人员来做,而开发人员也能从中受益。
没有真正意识到单元测试的收益,认为写单元测试太费时,不值得。
答:在开发时越早发现bug,就能节省更多的时间,降低更多的风险。单元测试先期要编写测试用例,是需要多耗费些时间,但是后面的调试、自测,都可以通过单元测试处理,不用手工一遍又一遍处理。实际上总时间被减少了。
项目经理或技术主管没有要求写单元测试,所以不用写。
答:写单元测试应该成为开发人员的一种本能,开发本身就应该包含单元测试。
不知道有单元测试这回事,不知道如何用。经过这篇文档的说明,就基本知道如何处理单元测试。
框架选型
常用单元测试框架:MSTest (Visual Studio官方)、XUnit 和 NUnit。
- MS Test为微软产品,集成在Visual Studio 2008+工具中。
- NUnit为.Net开源测试框架(采用C#开发),广泛用于.Net平台的单元测试和回归测试中,官方网址(www.nunit.org)。
- XUnit.Net为NUnit的改进版。
(以下主要讲解MSTest 和NUnit的使用,XUnit操作和NUnit操作基本类似)
基础实践
开始创建你的第一个的单元测试项目吧
1) 我们先来用 VS2017 中自带的测试模块(MSTest)来写一个简单的单元测试吧。
新建一个Solution,并添加项目UnitTestDemo(用于编写被测试的项目)
在该工程中添加UnitTestClass类,并书写一个静态的GetTriangle(string[] sideArr) 函数用来返回一个三角形的类型。
namespace UnitTest { public class UnitTestClass { ///
/// 获取三角形类型. /// /// 三角形三边长度数组. ///返回三角形类型名称. public static string GetTriangle(string[] sideArr) { string result = string.Empty; int a = int.Parse(sideArr[0]); int b = int.Parse(sideArr[1]); int c = int.Parse(sideArr[2]); if (a + b > c && a + c > b && b + c > a) { if (a == b && a == c) { result = "等边三角形"; } if (a == b || a == c || b == c) { result = "等腰三角形"; } else { result = "一般三角形"; } } else { result = "不构成三角形"; } return result; } } }然后在solution中添加一个UnitTestDemoTests测试项目,如图所示,添加 => 新建项目之后选择 测试 => 单元测试项目。
新建好测试项目之后,你会得到一个UnitTest1测试类模板,即一个带有[TestClass] attribute标记的类和一个带有[TestMethod] attribute标记的空方法**public void TestMethod1()**。
- 单元测试项目无法自动访问它正在测试的类库。 可以通过添加对类库项目的引用来提供测试库访问权限。 为此,请右键单击UnitTestProject1项目,然后依次选择“添加” > “引用”。在“引用管理器”对话框中,然后选择 UnitTestDemo项目,如下图中所示。
在UnitTestDemoTests项目中添加UnitTestDemo项目的引用,现在我们的solution就具有了下图所示的目录结构。
5. 在UnitTestDemoTests项目中的UnitTest1类中,将模板提供的样本单元测试代码替换为以下代码:
using UnitTest;
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace UnitTestDemoTests
{
[TestClass]
public class UnitTest1
{
[TestMethod()]
public void GetTriangle_Test()
{
string[] sideArr = {"5", "5", "5"};
Assert.AreEqual("等边三角形", UnitTestClass.GetTriangle(sideArr));
}
}
}
6. 生成UnitTestDemoTests测试项目,在生成项目后,测试项将出现在测试资源管理器中。 如果测试资源管理器窗口不可见,请选择顶级 Visual Studio 菜单上的“测试”,然后依次选择“**窗口(Windows)”、“测试资源管理器(Ctrl + E,T9)**”,如图所示。
7. 在测试资源管理器上可以看到刚刚所写的测试方法,这样在GetTriangle_Test单击右键选择“运行所选定的测试”就可以在Test Explorer里看到单元测试的运行结果,如下图所示。
可以看到,我们在单元测试中提供的例子的期望是输出“等边三角形”,运行结果却是“等腰三角形”。再看一看 GetTriangle() 函数的代码,原来是在对在判断三边数值是等边三角形之后没有使用 else if 又用 if 判断为等腰三角形了。通过这个简单的单元测试就能够发现一些意向不到的错误。不要以为这里的bug很低级,类似的情况确实会在现实中发生。
8. 把上面的错误更正后,再次运行TestMethod1()就会得到测试已通过的结果,如图所示。
创建单元测试项目和测试方法,除了以上通过手动创建单元测试项目和根据你的要求进行编写测试用例之外,还可以从你的项目的方法上直接生成单元测试项目和单元测试存根,那样操作更加方便,速度也会更快一些。
2) 通过代码直接生成单元测试项目和单元测试存根
- 在代码编辑器窗口中,从上下文菜单右键单击并选择“创建单元测试”。
- 在创建单元测试窗口,选择默认值,或更改用于创建并命名单元测试项目和单元测试的参数值。 单击“确定”,创建单元测试项目。
这里涉及测试框架的选择,MSTest是VS自带的测试框架。新的MS TEST现在是通过Nuget的包发布了,目前MS发布了两个版本:
- MS TEST V1:V1的版本依赖于一个包: MSTest.TestFramework
- MS TEST V2:V2的版本依赖于两个包: MSTest.TestFramework 和 MSTest.TestAdapter
这两个版本使用起来还是大同小异的,MSTest v2 主要是为了**.net core准备的,当然也可以在.net framework**上运行,并且在v1上新加入了一些扩展。
- 在生成的测试项目中,将测试代码添加到对应单元测试方法中,以使单元测试有意义。
- 此后生成测试项目,并在测试资源管理器中运行测试方法,得到测试结果(与上方步骤一致)。
编写测试代码
你使用的单元测试框架和 Visual Studio IntelliSense 将指导你完成为代码项目的单元测试编写代码。 若要在测试资源管理器中运行,大多数框架要求你添加特定的属性来识别单元测试方法。 框架还提供了一种方法,通常通过断言语句或方法属性,来指示测试方法是否已通过或失败。 其他属性标识可选的安装方法,即在类初始化时和每个测试方法和每个拆卸方法之前的安装方法,这些拆卸方法在每个测试方法之后和类被销毁之前运行。
AAA(准备、执行、断言)模式是编写待测试方法的单元测试的常用方法:
- 准备(Arrange),单元测试方法的准备部分初始化对象并设置传递给待测试方法的数据;
- 执行(Act),执行部分调用具有准备参数的待测试方法;
- 断言(Assert),断言部分验证待测试方法的执行行为与预期相同。
如示例中验证 UnitTest1.GetTriangle() 函数,我们编写了一个测试来验证方法的标准行为:
[TestMethod()]
public void GetTriangle_Test()
{
// arrange
string[] sideArr = { "5", "5", "5" }; // 准备传给待测试方法的数据
string expected = "等边三角形";
// act
var actual = UnitTestClass.GetTriangle(sideArr); // 调用测试方法
// assert
Assert.AreEqual(expected, actual); // 验证待测试方法的执行结果是否与预期相同
}
为单元测试设置超时值:
在某些情况下(例如通过网络获取数据),常常不希望程序卡住而占用太多时间,通过设置测试方法的超时时间,来测试一个方法是否在预期时间内执行。
[TestMethod()]
[Timeout(2000)] // 毫秒 要在单个测试方法上设置超时时间
public void GetTriangle_Test()
{ ...
}
[TestMethod()]
[Timeout(TestTimeout.Infinite)] // 毫秒 将超时时间设置为允许的最大值
public void GetTriangle_Test()
{ ...
}
MSTest参数化测试:
什么是参数化测试?
答:简单的说,就是同样的逻辑,根据输入参数不同,给出不同的结果。因为只是参数不同,所以并不希望把测试方法写多遍,但是又希望对每个参数的测试成为一个独立的测试用例。举例说,假定我有一个数学计算的方法是把两个整数相加求和,我希望证明这个方法对于任意两个数都是通过的。
在MSTest中可以通过DataRow Attribute 来指定测试用例的参数,实现参数化测试:
/// <summary>
/// 相加(待测试方法)
/// </summary>
/// <param name="num1">数值1</param>
/// <param name="num2">数值2</param>
/// <returns>计算结果</returns>
public static int Add(int num1, int num2)
{
return Math.Abs(num1 + num2);
}
/// <summary>
/// 测试方法
/// </summary>
[TestMethod()]
[DataRow(10, 20)]
[DataRow(-2, -5)]
[DataRow(1, -2)]
[DataRow(5, null)]
public void Add_Test(int num1, int num2)
{
Assert.AreEqual(UnitTestClass.Add(num1, num2), num1 + num2);
}
测试了所有可能的情况,以达到更好的覆盖率。上方给出示例Add方法的单元测试运行测试结果如下图所示。
测试结果:测试结果指出对两个数相加操作的方法,目标方法还取了绝对值,与相应结果不符。
测试调试
可以使用测试资源管理器为你的测试启动调试会话。 使用 Visual Studio 调试程序可以无缝地逐句得使你在单元测试和所测试项目之间来回反复。 若要开始调试:
在 Visual Studio 编辑器中,在想要调试的一个或多个测试方法中设置断点。
在测试资源管理器中,选择测试方法,然后点击右键从快捷菜单选择“调试选定的测试”。
3. 进入调试模式
F5 继续。
F10 执行下一行代码,但不执行任何函数调用。
F11 在执行进入函数调用后,逐条语句执行代码。
Shift + F11 执行当前执行点所处函数的剩余行。
Shift + F5 停止运行程序中的当前应用程序。可用于“中断”模式和“运行”模式。
NUnit 测试框架
1)下载安装NUnit插件
我们在VS中选择工具菜单栏下的扩展和更新,选择联机并在搜索框中输入NUnit。有2个版本的Nunit适配器,分别为NUnit 3.x(最新版为3.4.1)和NUnit 2.x(最新版为2.6.4),都支持Visual Studio 2012+。若想在VS2010中集成,需要安装NUnit 2.6.4安装包(可在官网下载)与VS2010 NUnit整合插件下载,下载安装完毕就能在 VS2010 的视图=>其他窗口中看到 Visual Nunit (或使用快捷键Ctrl + F7),打开该视图,将之拖到合适的位置。
2)创建NUnit单元测试项目
未完待续..
使用 Visual Studio 2017进行实时单元测试
Live Unit Testing 是 Visual Studio 2017 版本 15.3 中提供的一项技术,可在我们更改代码,然后保存的时候,它会自动生成自动测试,最后得出结果。
1)实时单元测试:
让你更有信心地对代码进行重构和更改。 Live Unit Testing 在编辑代码时自动执行所有受影响的测试,确保所做更改不会中断测试。
指示单元测试是否充分覆盖代码,并显示未被单元测试覆盖的代码。 Live Unit Testing 以图形方式实时描绘代码覆盖率,以便一眼就能看到每行代码覆盖的测试数,目和未被任何单元测试覆盖的行。
2)进行实时单元测试:
在类库项目中创建一个待测试方法,如下:
///
/// 相加(待测试方法) /// /// /// ///public static int Add(int num1, int num2) { return num1 + num2; } 根据以上创建单元测试项目的过程,创建一个单元测试项目(测试框架可以使用 Live Unit Testing 的 MSTest 测试框架(默认)。 还可使用 xUnit 和 NUnit 测试框架)。在测试项目添加对被测试类库项目的引用来提供测试库访问权限。
3. 在测试项目类中,将模板提供的样本单元测试方法代码替换为以下代码:
[TestMethod()]
[DataRow(10, 20)]
[DataRow(-2, -5)]
[DataRow(1, -2)]
[DataRow(5, null)]
public void Add_Test(int num1, int num2)
{
Assert.AreEqual(UnitTestClass.Add(num1, num2), num1 + num2);
}
4. 从顶级 Visual Studio 菜单中依次选择“测试” > “Live Unit Testing” > “启动” Visual Studio 启动 Live Unit Testing,使其自动运行所有测试。
5. 完成运行测试后,“测试资源管理器” 显示整体结果和各个测试的结果。 此外,代码窗口以图形方式显示测试代码覆盖率和测试结果。 如下图所示,三项测试均已成功执行。 它还显示测试中已覆盖 Add() 方法中的所有代码路径,并已成功执行这些测试(用绿色复选标记“✓”指示)。 UnitTestClass.cs 中的其他方法有部分代码没有代码覆盖率(用蓝线“—”指示)
还可通过在代码窗口中选择一个特定的代码覆盖率图标来获得有关测试覆盖率和测试结果的更多详细信息。 若要查看此详细信息,请执行以下操作:
单击行上的绿色复选标记“✓”, 如下图所示,Live Unit Testing 指示只有一个测试覆盖该行的代码,并且都已成功执行。
Live Unit Testing 中“—”标识的主要问题是代码覆盖率不完整,可以通过添加测试方法或改变测试参数,如下图,可以看到代码覆盖率已扩展到 GetTriangle() 的每一行代码。
在你修改源代码时,Live Unit Testing 将自动执行新增的和修改后的测试。
- 处理测试失败:
将 Add() 做些许修改,修改为计算两数相加的绝对值,在保存后,Live Unit Testing 指示 Add() 方法执行失败,如下图所示:
7. 停止实时单元测试:
使用 IntelliTest 为你的代码生成单元测试
IntelliTest 浏览你的 .NET 代码,以生成测试数据和单元测试套件。 对于代码中的每个语句,将生成执行该语句的测试输入。 为代码中的每个条件分支执行案例分析。 例如,分析 if 语句、断言和可能引发异常的所有操作。 此分析用于为你的每个方法生成参数化单元测试的测试数据,从而创建具有较高代码覆盖率的单元测试。
当你运行 IntelliTest 时,你可轻松看到哪些测试会失败,并可添加任何必要的代码来修复它们。 你可选择要保存到测试项目中的已生成测试,以提供回归套件。 当你更改代码时,重新运行 IntelliTest,以使生成的测试与你的代码更改同步。
IntelliTest 仅可用于 C# 且不支持 x64 配置。
IntelliTest 入门
若要生成单元测试,你的类型必须是公共类。 否则,先创建单元测试,然后再生成它们。
在 Visual Studio 中打开解决方案。 然后打开包含你要测试的方法的类文件。
在代码中右键单击一种方法并选择“创建 IntelliTest”,为方法中的代码创建生成单元测试项目。
接受默认格式以生成测试,或更改项目和测试的命名方式。 你可以创建新的测试项目或将你的测试保存到现有项目。
3. 创建测试项目成功之后,选择上图中“运行 IntelliTest”,为方法中的代码运行IntelliTest单元测试项目。
IntelliTest 使用不同的输入多次运行你的代码。 每次运行都会在表中表示出来,显示输入测试数据以及产生的输出或异常。
要为一个类中的所有公共方法生成单元测试,只需右键单击类而不是特定的方法。 然后选择“运行 IntelliTest”。 使用“浏览结果”窗口中的下拉列表,显示类中每个方法的单元测试和输入数据。
对于通过的测试,检查结果列中报告的结果是否与你对代码的预期要求匹配。 对于失败的测试,根据需要修复你的代码。 然后重新运行 IntelliTest 来验证修复。