
11.5 高级单元测试
前面学习了如何编写和执行单元测试。本节将继续学习如何为测试用例添加定制属性,以及如何使用同样的框架来测试私有方法和私有属性。
11.5.1 定制属性
借助测试框架为测试方法提供的各种测试特性,可以记录与测试用例相关的信息。可以在Properties窗口中对这些信息进行编辑,更新测试方法中相应的特性。有时需要指定自己定制的属性来驱动测试方法,这也可以使用Properties窗口进行设置。为此,在测试方法中添加TestProperty特性。例如,下面的代码为测试方法添加了两个特性,一个用于指定任意的日期,另一个用于指定期望的状态。这对于使用Test View窗口和Properties窗口进行的随机测试特别方便。
VB
<TestMethod()> <TestProperty("SpecialDate", "1/1/2008")> <TestProperty("SpecialStatus", "Suspended")> Public Sub SpecialCurrentStatusTest() Dim target As New Subscription target.PaidUpTo = CType(Me.TestContext.Properties.Item("SpecialDate"), _ Date) Dim val As Subscription.Status = _ [Enum].Parse(GetType(Subscription.Status), _ CStr(Me.TestContext.Properties.Item("SpecialStatus"))) Assert.AreEqual(val, target.CurrentStatus, _ "Correct status not set for Paidup date {0}", target.PaidUpTo) End Sub
C#
[TestMethod] [TestProperty("SpecialDate", "1/1/2008")] [TestProperty("SpecialStatus", "Suspended")] public void SpecialCurrentStatusTest() { var target = new Subscription(); target.PaidUpTo = this.TestContext.Properties["SpecialDate"] as DateTime?; var val = Enum.Parse(typeof(Subscription.Status), this.TestContext.Properties["SpecialStatus"] as string); Assert.AreEqual(val, target.CurrentStatus, "Correct status not set for Paidup date {0}", target.PaidUpTo); }
在Visual Studio 2010中,TestContext.Properties字典会自动填充。然而,Visual Studio 2012、2013和2015对单元测试进行了一些简化,现在必须手动编码值。
11.5.2 测试私有成员
单元测试的一个卖点是它对类内部的测试(以确保它们正常工作)特别有效。这里的假设是,如果每一个组件都是独立工作的,它们在一起正常工作的可能性就非常大。而事实上,单元测试可用于测试多个类的合作运行。那么,单元测试框架测试私有方法的效果如何呢?
.NET Framework的一个功能是它可以反射任意一个加载到内存中的类型,执行任意一个成员(无论它是私有的还是公有的)。这种功能会带来一定的性能损失,由于反射调用包含了一层额外的重定向操作,因此如果不断调用,就会对系统的运行造成很大的影响。尽管如此,对于测试来说,反射可以调用一个类的内部构造,而不需要担心这些调用所造成的潜在性能下降。
使用反射来访问类的非公有成员还存在另一个更大的缺陷:这种代码会变得非常混乱。在Subscription类上,为测试做准备:返回到CurrentStatus属性,把它的访问权限从public变更为private。
返回到单元测试,修改其主体,如下所示:
VB
<TestMethod(), _ DeploymentItem("Subscriptions.dll")> _ Public Sub Private CurrentStatusTest() ' Arrange Dim s = new Subscription() s.PaidUpTo = null ' Act Dim t = s.GetType() Dim result As Object Result = t.InvokeMember("CurrentStatus", BindingFlags.GetProperty | BindingFlags.Instance |BindingFlags.Public | BindingFlags.NonPublic, null, s, null) ' Assert Assert.IsInstanceOfType(result, GetType(Subscription.Status)) Assert.AreEqual(Subscription.Status.Temporary, Cast(result, Subscription.Status)) End Sub
C#
[TestMethod()] [DeploymentItem("Subscriptions.dll")] public void Private CurrentStatusTest() { // Arrange Subscription s = new Subscription(); s.PaidUpTo = null; // Act Type t = s.GetType(); object result = t.InvokeMember("CurrentStatus", BindingFlags.GetProperty | BindingFlags.Instance |BindingFlags.Public | BindingFlags.NonPublic, null, s, null); // Assert Assert.IsInstanceOfType(result, typeof(Subscription.Status)); Assert.AreEqual(Subscription.Status.Temporary, (Subscription.Status)result); }
可以看到,上面的例子以InvokeMember方法的形式使用了反射。特别地,它检索类型(这将是Subscription类),然后调用InvokeMember来检索(GetProperty绑定标志)CurrentStatus属性值。接下来,判断结果是否为Subscription.Status类型并等于Temporary。