单元测试目的是为了保证代码按照程序员的预期运行,往往关注的是比其他测试更低的层次,从而确定代码的底层功能是否与期望一致。
对于API框架而言,单元测试的一个重要优点在于测试在编译阶段就引入了依赖关系,从而使之更容易确定代码的更改是否会影响API表示的契约。
单元测试程序集与被测代码一般要分离开,这样可以保证在对应用程序代码进行部署时不包含单元测试,因为单元测试代码在产品环境中没有什么用。
代码的独立单元测试越容易,就越容易维护。
所有测试都应该是自主的和独立的,且应该对期望行为和错误情况都进行测试。
假设有以下内容需要被测:
1 namespace ExtendedMath 2 { 3 public static class Functions 4 { 5 public static int Fibonacci(int factor) 6 { 7 if (factor < 2) 8 { 9 return (factor); 10 } 11 int x = Fibonacci(--factor); 12 int y = Fibonacci(--factor); 13 return x + y; 14 } 15 } 16 }
接下来创建单元测试验证Fibonacci实现,只有包含在独立的test project项目中之后,单元测试才会被称为测试。
创建一个单元测试项目,添加上述代码的命名空间引用,且在测试类最上方添加引用:
1 using ExtendedMath; 2 using Microsoft.VisualStudio.TestTools.UnitTesting;
为了使Visual Studio识别包含单元测试的类,必须给TestClass特性赋值,如果忘记添加TestClass特性,那么将找不到类中的单元测试方法。单元测试需要存放在公有的类中,因此一定要将类标记为public,单元测试方法必须是公有的和非静态的,而且没有参数和返回值,为了区别单元测试方法与普通方法,需要使用TestMethod特性。此时我们看到代码如下:
1 namespace ExtendedMath 2 { 3 [TestClass] 4 public class FunctionsTest 5 { 6 [TestMethod] 7 public void FibonacciTest() 8 { 9 } 10 } 11 }
单元测试是通过抛出异常的方式向Visual Studio指示失败的。未抛出异常的测试都认为是通过的,除非测试针对的是ExpectedException特性,单元测试框架定义了Assert对象。在FibonacciTest()方法中添加代码:
1 [TestMethod] 2 public void FibonacciTest() 3 { 4 const int FACTOR = 8; 5 const int Expected = 21; 6 int actual = ExtendedMath.Functions.Fibonacci(FACTOR); 7 8 Assert.AreEqual(Expected, actual); 9 }
在编写单元测试方法时,3A模式很有帮助——Arrange, Act, Assert. 首先建立变量安排测试,然后测试调用代码,最后断言调用代码是否与期望值一致。对于每个A都应该建立相对独立的代码段。
使用TestInitialize特性创建一个方法,该方法将会在当前类的每个单元测试方法执行之前执行一次。类似的,TestCleanup总是在每个测试执行之后立即执行。与单元测试类似,包含有这些特性的方法必须是公有的和非静态的,并且没有参数和返回值。TestInitialize运行之后立即运行单元测试,单元测试运行之后立即运行TestCleanup。
无论进行多少次单元测试ClassInitialize和ClassCleanup特性均在当前类中只运行一次。这两个方法是静态的,并接受一个TestContext实例作为参数。
可以使用ClassInitialize和ClassCleanup控制类一级的操作,而对于程序集一级的操作则需要用到AssemblyInitialize和AssemblyCleanup特性。例如,用AssemblyInitialize修饰的方法会在当前程序集的任意一个测试执行之前执行,而不是仅仅针对当前类中的测试。这些方法也必须是静态的,且接受一个TestContext类型的参数。
当多个类之间有公共操作时,就可以考虑使用AssemblyInitialize和AssemblyCleanup特性,但此时并不是针对每个类都调用初始化和清除方法,而是使用程序集一级的方法。
确定单元测试成功与否的常用方法是将期望结果与实际结果进行比较。
Assert.AreSame/Asser.AreNotSame常见用法是保证属性返回期望的实例,或集合能正确处理引用,它们用来验证两个参数状态相同的情况下实际上是否指向同一个对象。
通常,单元测试都会有一个TestContext实例的引用,该对象用于向测试提供运行时的功能,如测试本身的详细信息、所用的各种目录以及提供存储在测试结果中详细信息的方法等。
对代码行为进行验证的最好的方法是利用真实的数据执行代码。Visual Studio提供了为单元测试自动绑定数据源作为输入的功能。单元测试将会对每个数据行都运行一次。
如何对类的非公有成员进行测试呢?假如正在编写一个私有函数,它不可以被公开访问,而只能由内部其他成员访问,我们想要测试该方法,但测试代码却无法访问私有成员,通常有4种方法处理:
1. 将需要测试的私有成员更改为公有成员
2. 将需要测试的私有成员更改为内部成员,并将测试程序集添加到原始程序集的internalsVisibleTo特性中
3. 保证需要测试的私有成员能够通过公有成员访问,然后通过那些公有成员进行测试
4. 在测试中用.NET反射加入并直接调用那些非公有成员
假设要对下面这个类的私有数据字段和方法进行测试:
1 public class Example 2 { 3 public Example() 4 { } 5 private string password = "letmein"; 6 private bool VerifyPassword(string password) 7 { 8 return (string.Compare(this.password, password, false) == 0); 9 } 10 }
Visual Studio引入了PrivateObject类,它对反射代码进行了封装,通过这个类可以以一种非常直接的方式访问非公有成员。要使用这个类,首先需要创建该类的一个实例,其参数是所需处理的类的Type对象:
1 namespace Explorations 2 { 3 [TestClass] 4 public class ExampleTest 5 { 6 private PrivateObject privateObject; 7 const string PASSWORD = "letmein"; 8 9 [TestInitialize] 10 public void TestInitialize() 11 { 12 privateObject = new PrivateObject(typeof(Example)); 13 } 14 } 15 }
接下来就可以创建测试了:
1 namespace Explorations 2 { 3 [TestClass] 4 public class ExampleTest 5 { 6 private PrivateObject privateObject; 7 const string PASSWORD = "letmein"; 8 9 [TestInitialize] 10 public void TestInitialize() 11 { 12 privateObject = new PrivateObject(typeof(Example)); 13 } 14 15 [TestMethod] 16 public void ComparePrivatePassword() 17 { 18 string password = (string)privateObject.GetField("password"); 19 Assert.AreEqual(PASSWORD, password); 20 } 21 22 [TestMethod] 23 public void TestPrivateVerifyPassword() 24 { 25 bool accepted = (bool)privateObject.Invoke("VerifyPassword", PASSWORD); 26 Assert.IsTrue(accepted); 27 } 28 } 29 }
由于PrivateClass使用了反射,因此,必须将调用结果从通用Object类型转换成正确的类型。
PrivateObject用于访问类的实例成员,如果要访问非公有静态成员,则需要用到PrivateType类,它的接口与PrivateObject非常类似,也对反射代码进行了封装。
Visual Studio单元测试功能全面支持代码覆盖,代码覆盖会自动插入跟踪逻辑(插装过程)以监视测试执行过程中哪些代码执行到了。它最重要的作用就是发现那些在测试中没有涉及到的代码段。
说明单元测试的有效性的一条原则是,删除任何一行代码都会导致至少一个单元测试的失败。
启用代码覆盖并选中了要插装的程序集之后,接下来运行测试,从Code Coverage Results窗口中可以浏览覆盖的结果:
100%的代码覆盖并不意味着单元测试已充分了,正确的测试需要使用不同的数据对相同的代码进行多次测试。代码覆盖是测试有效性的一种度量手段,但肯定不是唯一的。
使用测试影响分析可以在修改代码之后浏览哪些测试受到了代码更改的影响,这不仅限于单元测试,甚至适用于那些先前已执行的针对某个生成的手动测试。
在使用测试影响分析之前,首先必须在VS内使用单元测试框架,并将源代码存储在TFS Version Control中,然后在运行单元测试时启用数据收集功能。如果单元测试是在本地或通过MTM(Microsoft Team Manager)运行的,那么就必须将测试结果上传到TFS并将其与一个团队生成相关联之后才能进行测试影响分析。这里不做过多介绍。