Visual Studio 单元测试扩展






2.75/5 (4投票s)
一篇关于创建单元测试扩展的文章。

引言
VSTest 是一个功能强大的单元测试框架,它与 Visual Studio Team System 紧密集成。然而,仍然有改进的空间,而通过使用一些新的 C# 语言特性来扩展框架是一个很好的方法。
背景
测试驱动开发 (TDD) 已经存在一段时间了。在 .NET 中有许多支持这种开发方法的框架,例如 NUnit、MbUnit 和随 Visual Studio Team System 2005 一起推出的 VSTest。
最近,TDD 正在经历一场向行为驱动开发 (BDD) 的转变。这种较新的开发方法是对 TDD 的微妙改进。在 BDD 中,重点是规范,而不是测试。这主要是视角的变化,而不是方法论的根本性改变。取而代之的是测试,您拥有规范。取而代之的是测试方法,您拥有行为规范。
遵循 BDD 时仍然可以使用传统的 TDD 库,尽管趋势是更改库以使其遵循较新的术语。这有助于开发人员,特别是初学者,专注于指定行为,而不是专注于测试。有一些较新的 .NET 库采取了这种方法,例如 NSpec、NBehave 和 NSpecifiy。
在我正在撰写的一篇文章中,我使用了 VSTest 并尝试进行 TDD。作为一个对这种方法论的新手,过去我总是先编写代码,然后创建单元测试,我发现整个概念很难。在研究这个主题以确定原因时,我开始意识到我编写测试时关注的是尚未存在的代码的实现细节,而不是我期望的行为。我认为这正是 BDD 的意义所在。以正确的方式思考开始使遵循“先测试”的软件开发方式变得更容易(我不会声称它很容易)。
我曾考虑切换到其他 BDD 特定测试框架,但我真的很喜欢 VSTest 的集成。BDD 测试框架和 TDD 框架之间唯一的真正区别是词汇。意识到这一点,我注意到在 VSTest 中,强制规定的词汇非常少。主要有 TestClassAttribute
、TestMethodAttribute
和 Assert
。这些都使用了 BDD 希望我们避免使用的词汇。然而,对我来说,这两个属性名称不是大问题,只要您以更符合规范的方式命名您的测试方法即可。更改属性名称会更好,特别是对于 BDD 初学者,但实际上并非必要。另一方面,Assert
类确实让我有些头疼。
首先,从实际角度来看,Assert
类存在一个简单的事实,即它们不完整。虽然您可以使用 VSTest 进行许多断言,但其他框架都 提供了更多。添加断言来填补空白实际上会很麻烦。一个简单的例子是,当您发现 StringAssert
没有 DoesNotContain
方法时。要添加此断言,您必须创建自己的断言类,而问题就在于:您会如何称呼它?即使在其自己的命名空间中,由于名称的重用,您也无法使用 StringAssert
这个名称而不引起使用困难。任何其他名称都可能令人困惑或过于冗长。
其次,从 BDD 的角度来看,断言的名称已经很糟糕了。BDD 希望您的规范以“流畅”的方式编写,尽可能接近纯英文。这不仅使规范更易于阅读,还可以使用工具从代码中提取规范,并生成非开发人员可读的文档。
这些因素促使我开发了这些扩展。
Using the Code
这些扩展提供了一个框架,可以轻松地与 VSTest 框架一起使用。只需添加对 CodeProject.VisualStudio.QualityTools.UnitTestFramework
程序集的引用即可。
从这个框架中使用的主要类是 Specify
静态类。该类中的两个静态方法(带有针对特定类型的逻辑重载)提供了任何规范的起点:That
和 ThatAction
。这些方法返回一个 SpecificationValue
,它是一个方便的类型,用于编写描述实际规范的扩展方法。
扩展方法是 C# 3.5(及其他 .NET 语言)中的一个新概念。它们只是静态方法,其第一个参数带有 this
关键字限定。编译器允许您使用与调用对象成员完全相同的语法来调用这些扩展方法,即使它们不是成员函数。例如,Specify
有一个名为 ShouldEqual
的成员。
public static void ShouldEqual<T>(this SpecificationValue<T> self,
object expected)
{
// code removed for brevity
}
有了这个扩展方法,我们可以编写一个规范,在某些给定条件下,我们类上的某个属性应该具有特定值,通过编写非常流畅的代码。
[TestMethod]
public void SomeProperty_should_construct_with_the_value_xyzzy()
{
SomeObject target = new SomeObject();
Specify.That(target.SomeProperty).ShouldEqual("xyzzy");
}
该框架已经包含了其他单元测试库中发现的大多数典型规范。但是,如果您发现需要新的规范,很容易创建一个以 SpecificationValue
作为第一个参数的扩展方法,如果规范失败,该方法会抛出正常的 AssertFailedException
。SpecificationValue
是一个泛型类型,由于这个原因,您在编写扩展方法时需要遵循一些规则。
- 如果规范应该适用于任何类型,请使扩展方法泛型,并使用一个泛型参数来指定
SpecificationValue
的类型。 - 如果规范应该接受特定类型,或者从该类型派生的类型(或实现该接口类型),请使扩展方法泛型,并使用一个泛型参数来指定
SpecificationValue
的类型,然后使用泛型约束来约束类型。 - 如果规范应该接受一个特定类型,该类型是密封类型或内置类型,请不要使扩展方法泛型,而是指定确切的
SpecificationValue
类型。
如果您查看框架的代码,您会发现这些规则的应用方式多种多样。
除了 Specify
类之外,框架中还包含了一些我发现在创建数据模型时很有用的专用类。PropertyChangedWatcher
可以轻松地指定实现 INotifyPropertyChanged
的对象上的属性在修改时应引发 PropertyChanged
事件。
[TestMethod]
public void SomeProperty_should_raise_PropertyChanged_when_modified()
{
SomeObject target = new SomeObject();
PropertyChangedWatcher watcher = new PropertyChangedWatcher(target);
target.SomeProperty = "xyzzy";
Specify.That(watcher).ShouldHaveSeen("SomeProperty");
}
CollectionChangedWatcher
为实现 INotifyCollectionChanged
的集合提供了类似的功能。
最后,TestExtensions
提供了一个名为 SerializeClone
的扩展方法,该方法有助于测试可序列化对象。
[TestMethod]
public void SomeObject_should_be_serializable()
{
SomeObject target = new SomeObject();
string expectedValue = "xyzzy";
target.SomeProperty = expectedValue;
SomeObject clone = target.SerializeClone();
Specify.That(clone.SomeProperty).ShouldEqual(expectedValue);
}
关注点
该框架为编写 BDD 规范提供了一个很好的词汇,同时继续使用 VSTest,因为它与 IDE 紧密集成。然而,它并没有提供一个完整的 BDD 框架,因为我们仍然依赖 VSTest 框架进行测试,这可能会让 BDD 纯粹主义者感到不满。如果 VSTest 能为我们提供更多的可扩展点,以便我们能够完全移除“测试”的词汇,那就更好了,但目前我认为没有办法做到这一点。
除了提供更好的规范词汇之外,该框架还通过使用 .NET 3.5 中的新扩展方法,使扩展规范变得更加容易。这是整个框架的关键。
历史
- 2007-12-8: 初始发布