SpecsFor 是一个具备高度灵活性的框架,它能够为.NET 应用程序实现优雅的单元测试与集成测试。在本文中,你将学习到上手使用 SpecsFor 所需的所有知识,包括对测试执行器(test runner)的选择,以及如何创建你的第一个测试项目的全部过程。
你还将看到,SpecsFor 如何避免在测试中常见的困难与挑战,让你得以专注于对重要的逻辑进行测试,而不是将精力消耗在无聊的准备过程上。
SpecsFor 概述
我希望你所问的第一个问题是:“SpecsFor 到底是什么?”。它是一种测试框架,设计它的目的是通过抽象的方式消除测试过程中各种令人烦恼的关注点,让你得以快速地编写整洁的测试。它不仅十分灵活,而且也具有高度的扩展性。
SpecsFor 可以使用在几乎任何一种类型的自动化.NET 测试过程中,包括单元测试、集成测试,以及全面的端到端测试。
或许让你了解 SpecsFor 最好的方法就是看看它是如何实际运行的。以下代码是某个 MVC 汽车工厂(car factory)项目的一个 spec,其中所使用的是由 MS Test 和某个模拟(mock)框架所提供的工具。
[<span color="#0080c0">TestClass</span>] <span color="#0000ff">public class</span> <span color="#0080c0">CarFactorySpecs</span> { [<span color="#0080c0">TestMethod</span>] <span color="#0000ff">public void</span> it_calls_the_engine_factory() { <span color="#0000ff">var</span> engineFactoryMock = <span color="#0000ff">new</span> <span color="#0080c0">Mock</span><<span color="#0080c0">IEngineFactory</span>>(); <span color="#0000ff">var</span> sut = <span color="#0000ff">new</span> <span color="#0080c0">CarFactory</span>(engineFactoryMock.Object); sut.BuildMuscleCar(); engineFactoryMock.Verify(x => x.GetEngine("<span color="#800000">V8</span>")); } [<span color="#0080c0">TestMethod</span>] <span color="#0000ff">public void</span> it_creates_the_right_engine_type_when_making_a_car() { <span color="#0000ff">var</span> engineFactoryMock = <span color="#0000ff">new</span> <span color="#0080c0">Mock</span><<span color="#0080c0">IEngineFactory</span>>(); engineFactoryMock.Setup(x => x.GetEngine("V8")) .Returns(<span color="#0000ff">new</span> <span color="#0080c0">Engine</span> { Maker = <span color="#800000">"Acme"</span>, Type = <span color="#800000">"V8"</span> }); <span color="#0000ff">var</span> sut = <span color="#0000ff">new</span> <span color="#0080c0">CarFactory</span>(engineFactoryMock.Object); <span color="#0000ff">var</span> car = sut.BuildMuscleCar(); <span color="#0080c0">Assert</span>.AreEqual(car.Engine.Maker, <span color="#800000">"Acme"</span>); <span color="#0080c0">Assert</span>.AreEqual(car.Engine.Type, <span color="#800000">"V8"</span>); } }
仅仅为了测试几个简单的东西,就需要编写这么多代码。
现在再让我们来看看,对于同样的 spec,使用 SpecsFor 写出的测试是怎样的:
<span color="#0000ff">public class</span> <span color="#0080c0">when_creating_a_muscle_car</span> : <span color="#0080c0">SpecsFor</span><<span color="#0080c0">CarFactory</span>> { <span color="#0000ff">protected override void</span> Given() { GetMockFor<<span color="#0080c0">IEngineFactory</span>>() .Setup(x => x.GetEngine(<span color="#800000">"V8"</span>)) .Returns(<span color="#0000ff">new</span> <span color="#0080c0">Engine</span> { Maker = <span color="#800000">"Acme"</span>, Type = <span color="#800040">"V8"</span> }); } <span color="#0000ff">private</span> <span color="#004080">Car</span> _car; <span color="#0000ff">protected override void</span> When() { _car = SUT.BuildMuscleCar(); } [<span color="#0080c0">Test</span>] <span color="#0000ff"> public void</span> then_it_creates_a_car_with_an_eight_cylinder_engine() { _car.Engine.ShouldLookLike(() => <span color="#0000ff">new </span><span color="#0080c0">Engine</span> { Maker = <span color="#800000">"Acme"</span>, Type = <span color="#800000">"V8"</span> }); } [<span color="#0080c0">Test</span>] <span color="#0000ff"> public void</span> then_it_calls_the_engine_factory() { GetMockFor<<span color="#0080c0">IEngineFactory</span>>() .Verify(x => x.GetEngine("V8")); } }
这里的重点不在于代码中“有什么”,而是在于代码中“没有什么”:
- 没有重复性的准备过程。实际上,这里完全使用任何代码用于创建工厂,这是由 SpecsFor 本身所完成的。
- 没有用于跟踪 mock 对象的代码,这依然是由 SpecsFor 本身实现的。
整个代码中只有一些简短的准备过程,和一些相关的测试用例。
我们会在本文的稍后部分对使用 SpecsFor 编写的这个 spec 如何运行的机制进行分析,但现在,还是让我们来看一看 SpecsFor 是怎样产生的。
站点巨人的肩膀上
SpecsFor 的出现是建立在一系列优秀的开源工具的基础上的。它将这些工具结合在一个软件包中,帮助你快速地克服在测试中经常出现的障碍。
SpecsFor 的核心是在 NUnit 的基础上建立的,这就意味着支持 NUnit 的任何一种测试执行器或构建服务器也同时支持 SpecsFor,而不需要安装独立的插件或是安装包。
其次,SpecsFor 中还提供了 Should ,这个类库中包含了大量的扩展方法,用于进行常见的测试断言。通过使用这个类库,你就不必编写那些读上去有些拗口的断言,例如“Assert.AreEqual(x, 15)”等等,而可以写出可读性良好的断言,例如“x.ShouldEqual(15)”。这本身是个很小的改动,但造成的影响却是巨大的!
模拟(mocking)这个话题在测试社区中总是伴随着巨大的争论,但 mock 确实可以成为一种有用的工具。因此,SpecsFor 也包含了 Moq ,这个类库能够帮助你在测试中很简单地创建 mock 与 stub(桩)对象。
伴随着 Moq 的引入,SpecsFor 中同时包含了一个自动进行模拟的容器,它是在 StructureMap 的基础上创建的。通过使用这个容器,SpecsFor 能够在创建你将进行测试的类的同时,自动创建这个类的所有依赖。
最后,SpecsFor 中还提供了一个解决方案,它能够处理我在.NET 测试过程中所见过的最常见的(也是最烦人的)一个问题:即对象的比较。在默认的对象相等性实现方式中,只有当 X 和 Y 两个对象指向内存中的同一个实例时,才会返回 true。因此,下面这个 spec 测试会失败:
[<span color="#0080c0">Test</span>] <span color="#0000ff">public void</span> equal_people_test() { <span color="#0000ff">var</span> person1 = <span color="#0000ff">new</span> <span color="#0080c0">Person</span> {Name = <span color="#800000">"Bob"</span>, Age = 29}; <span color="#0000ff">var</span> person2 = <span color="#0000ff">new</span> <span color="#0080c0">Person</span> {Name = <span color="#800000">"Bob"</span>, Age = 29}; <span color="#0080c0">Assert</span>.AreEqual(person1, person2); }
解决方案无非有两种,要么对 Equals 方法进行重载,让它使用你自己提供的版本,要么将对象的每个属性进行比较。这两种方式都有着难以维护的缺点。
通过使用 ExpectedObjects 这个优秀的框架,可以将上面的 spec 以这种方式进行重写:
[<span color="#0080c0">Test</span>] <span color="#0000ff">public void</span> equal_people_test() { <span color="#0000ff">var</span> person1 = <span color="#0000ff">new</span> <span color="#0080c0">Person</span> {Name = <span color="#800000">"Bob"</span>, Age = 29}; <span color="#0000ff">var</span> person2 = new Person {Name = <span color="#800000">"Bob"</span>, Age = 29}; person1.ShouldEqual(person2); }
你甚至还可以进行部分匹配(只对你所关心的那部分属性进行比对)。我们在将本文的稍后部分对此进一步展开讨论!现在,让我们回过头看看如何开始使用 SpecsFor 这个工具吧。
基本使用方式
由于 NuGet 这样的工具的存在,开始使用 SpecsFor 也变得非常简单。不过,在你开始之前,你还需要一个测试执行器,它将用于执行你的测试用例。你或许已经有一个测试执行器了(如今的大多数.NET 开发者都会进行某种形式的自动化测试了),但如果你还没有的话,我建议你尝试一下以下几种可选的工具。
ReSharper 的测试执行器 —— ReSharper 是我最喜爱的生产力工具之一,其中还包含了一个测试执行器,能够执行 NUnit 测试,因此它也能够用于测试你用 SpecsFor 所创建的 spec。
TestDriven.NET —— 这是目前为止历时最久的(也是最成熟的).NET 测试执行器。它可以执行使用任何一种框架创建的测试。它的 UI 非常简单,这一点或许是你想要的,或者不是,但它还是值得一试的!
NCrunch —— 大多数测试执行器都需要你手动启动某个测试的执行,而 NCrunch 则不同:它能够在你编写代码的同时,在后台自动执行你的测试。这个工具可能并不便宜,但它能够节省你大量的时间。
Visual Studio ——Visual Studio 专业版及更高版本中实际上自带一个测试执行器。通过使用 NUnit Adapter ,它也能够完美地运行 NUnit 测试用例。这个测试执行器在我心目中只能排在末尾,不过……它是免费的!
现在让我们来看看,如何在一个现有的解决方案中加入 SpecsFor。我将所有 spec 都添加到一个简单的类库中,以方便你在 GitHub 上直接查看。你可以选择直接在代码库中的解决方案中运行,或自行创建一个解决方案。假设你已经在 Visual Studio 中打开了这个解决方案,请在解决方案中加入一个新的类库(C#)项目。这个项目将用于编写我们的 spec,我通常会为该项目使用某种简单的命名规范,例如“SolutionName.Specs”。
你的项目中会生成一个无用的 Class1.cs 文件。把它干掉,我们这里用不着它!
我们还需要确保让这个 spec 项目引用准备接受测试的项目,请再次右键单击“引用”,选择“添加引用”,再选择“解决方案”,然后选中所要进行测试的项目。
(单击图片以放大)
接下来就要在项目中添加 SpecsFor 类库了。右键单击“引用”并选择“管理 NuGet 包”,然后就会打开 NuGet 包管理器。可以在线搜索“SpecsFor”,找到 “SpecsFor”这个包(暂时可以忽略其它相关的包)并选择安装!
(单击图片以放大)
在你安装 SpecsFor 的过程中,它会自动安装 SpecsFor 中所包含的那些实用类库,因此你现在一共有了 SpecsFor、NUnit、Moq、Should、ExpectedObjects 和 StructureMap.AutoMocking 这几个类库!
现在,我们已经准备好创建第一个 spec 了。如果你使用了源代码中的项目文件,你会看到在领域中有一个名为 Car 的对象。让我们编写一些 spec,看一看当运行中的车辆停下来的时候会发生些什么事。
提示:我将会按照我个人的喜好设定命名规范与代码组织规范,但 SpecsFor并不在乎 spec的命名规范与组织规范,可以自行选择最适合你的方式!
在你的 spec 项目中创建一个新类 CarSpecs。在这个类中加入我们需要用到的命名空间:NUnit.Framework、Should、SpecsFor,以及我们进行测试的类所属的命名空间(在这个示例项目中,这个命名空间就是 InfoQSample.Domain)。现在你的类看起来应该像下面这样:
<span color="#0000ff">using</span> NUnit.Framework; <span color="#0000ff">using</span> Should; <span color="#0000ff">using</span> SpecsFor; <span color="#0000ff">using</span> InfoQSample.Domain; <span color="#0000ff">namespace</span> InfoQSample.Specs { <span color="#0000ff">public class</span> <span color="#0080c0">CarSpecs</span> { } }
现在我们准备好编写 spec 了!
在定义测试用例时,SpecsFor 遵循 Given-When-Then 这一套 BDD 语言。这套语言是由三个部分所组成的:
- Given:设定你的状态。在你执行 spec 正处于什么样的状态下?
- When:施加某个行为。调用你的测试目标中的方法。
- Then:验证。确保该行为是按照你的希望进行的。
以下是我们准备实现的 spec,它可以用相同的语言进行编写,文字表述如下:
Given 一辆汽车正在运行
When 这辆车准备停下
Then 这辆车停下来了
Then 发动机也停止运转了
SpecsFor 中的 Spec 都是派生于 SpecsFor
<span color="#0000ff">public class</span> <span color="#0080c0">CarSpecs</span> { <span color="#0000ff">public class</span> <span color="#0080c0">when_a_car_is_stopped</span> : <span color="#0080c0">SpecsFor</span><<span color="#0080c0">Car</span>> { } }
提示:再次提醒,我个人喜欢将每个场景内嵌在 spec__ 这个大类中,但这种组织方式完全是可选的。
虽然这段代码看起来没有什么惊人之处,但由于 spec 类派生于 SpecsFor
下面让我们来实现这个 spec。首先,我们需要设定当前的状态:“Given__ 一辆汽车正在运行……”
<span color="#0000ff">protected override void</span> Given() { <span color="#008000">//Given a car is running (start the car!)</span> SUT.Start(); }
注意,SpecsFor 中提供了一个 virtual 方法 Given,我们可以对此进行重写,以设定我们的状态。SpecsFor 会自动调用一次该方法,以确保当前状态是我们所期望的。在我们的这个 spec 中,这段代码会启动我们的车辆,因此我们现在就有了一辆正在运行中的车子,可以对其施加行为了。
现在让我们施加某个行为,“When__ 这辆车准备停下……”
<span color="#0000ff">protected override void</span> When() { <span color="#008000">//When a car is stopped</span> SUT.Stop(); }
SpecsFor 也提供了一个 virtual 方法 When,我们可以在其中加入方法。现在我们的车正在停下
!那么结果应该是什么样的呢?“Then 这辆车停下来了……”
[<span color="#0080c0">Test</span>] <span color="#0000ff">public void</span> then_the_car_is_stopped() { SUT.IsStopped.ShouldBeTrue(); }
这里没有可以进行重写的基方法,因为你的 spec 中通常来说需要验证多个内容。因此,我们需要添加一个 public 方法,并用 NUnit 中的 Test 属性进行修饰。请注意我们是如何验证结果的正确性的:我们使用了 Should 类库中所提供的 fluent 扩展方法。
按照我们的 spec 中的声明,应该有两个为 true 的结果,因此我们再次添加一个测试用例:
[<span color="#0080c0">Test</span>] <span color="#0000ff">public void</span> then_the_engine_is_stopped() { SUT.Engine.IsStopped.ShouldBeTrue(); }
我们再次添加了一个 public 方法,用 Test 属性进行装饰,随后使用 Should 类库中的 fluent 扩展方法验证结果与我们的期望是否相同。
现在,你可以在你自己选择的测试执行器中运行你的 spec 了,你应该看到每个测试都顺利通过!
完整的 spec 如下所示:
<span color="#0000ff">using </span>Should; <span color="#0000ff">using</span> SpecsFor; <span color="#0000ff">using</span> InfoQSample.Domain; <span color="#0000ff">namespace</span> InfoQSample.Specs { <span color="#0000ff">public class</span> <span color="#0080c0">CarSpecs</span> { <span color="#0000ff">public class</span> <span color="#0080c0">when_a_car_is_stopped</span> : <span color="#0080c0">SpecsFor</span><<span color="#0080c0">Car</span>> { <span color="#0000ff">protected override void</span> Given() { <span color="#008000">//Given a car is running (start the car!)</span> SUT.Start(); } <span color="#0000ff">protected override void</span> When() { <span color="#008000">//When a car is stopped</span> SUT.Stop(); } [<span color="#0080c0">Test</span>] <span color="#0000ff">public void</span> then_the_car_is_stopped() { SUT.IsStopped.ShouldBeTrue(); } [<span color="#0080c0">Test</span>] <span color="#0000ff">public void</span> then_the_engine_is_stopped() { SUT.Engine.IsStopped.ShouldBeTrue(); } } } }
我在这里依然想提醒你的是,这个 spec 中“所没有”的东西:这里没有任何代码用于创建 Car 的实例,它是自动完成的。我们也不需要在每个测试用例中都调用启动和停止的方法,因为我们能够使用 SpecsFor 中的 Given 和 When 方法,将这些操作进行封装。在这个 spec 中,我们只留下了感兴趣的部分,而没有其它任何多余的代码。
当然,这个 spec 确实很简单。即使不使用 SpecsFor,这个 spec 中的代码也不会显得很多。接下来让我们看看测试一个更复杂的类的情形。
Mock 与自动 Mock
SpecsFor 最大的一点优势就在于,你不需要处理待测系统的依赖,SpecsFor 会替你处理它们。如果依赖本身是抽象类或接口,那么 SpecsFor 会为你自动创建 mock 对象,并且对这些对象进行追踪,并且允许你在 spec 和待测系统中使用这些对象。让我们看看,如何利用这一功能为我们这个示例项目中的 CarFactory 类编写一个 spec。
首先,让我们看看这个 spec 的语言表述:
When 创建一台肌肉车
Then 它将创建一台具有八缸发动机的汽车
Then 它将从发动机工厂中获取发动机实例
这个 spec 需要一些额外的准备工作。看一下我们的 CarFactory 类,我们将发现它依赖于一个类型,该类型需要实现 IEngineFactory 接口:
<span color="#0000ff">public class</span> <span color="#0080c0">CarFactory </span>{ <span color="#0000ff">private readonly</span> <span color="#0080c0">IEngineFactory</span> _engineFactory; <span color="#0000ff">public</span> CarFactory(<span color="#0080c0">IEngineFactory</span> engineFactory) { _engineFactory = engineFactory; } <span color="#0000ff">public</span> <span color="#0080c0">Car</span> BuildMuscleCar() { <span color="#0000ff">return new</span> <span color="#0080c0">Car</span>(_engineFactory.GetEngine(<span color="#800000">"V8"</span>)); } }
SpecsFor 会为我们自动创建一个 IEngineFactory 的 mock 对象(使用 Moq),并在加载 SUT 属性的时候会将这个 mock 对象提供给我们的工厂。我们要做的只是对 mock 进行配置,确保它的行为与我们期望的相同。对于如何使用 Moq 对象的完整讨论已经超出了本文的范围,但你可以查看 Moq 的文档,并通过更多的示例进行学习。
在我们这个 spec 中,我们只需要告诉这个 mock 的 IEngineFactory 对象,让它返回一个发动机即可。具体的步骤是首先让 SpecsFor 为我们提供这个 mock 对象,然后对其进行配置。我们将在 spec 中的 Given 方法中实现这一过程。
<span color="#0000ff">protected override void</span> Given() { GetMockFor<<span color="#0080c0">IEngineFactory</span>>() .Setup(x => x.GetEngine(<span color="#800000">"V8"</span>)) .Returns(<span color="#0000ff">new</span> <span color="#0080c0">Engine</span> { Maker = <span color="#800000">"Acme"</span>, Type = <span color="#800000">"V8" </span> }); }
我们将通过 GetMockFor
现在我们可以继续编写 spec 了,我们需要实现 When 方法:
<span color="#0000ff">private</span> <span color="#0080c0">Car</span> _car; <span color="#0000ff">protected override void</span> When() { _car = SUT.BuildMuscleCar(); }
我们在这里调用了 BuildMuscleCar 方法,它将为我们返回一个 Car 的实例。由于我们的测试用例需要对这个 car 进行操作,因此我们将它的值赋给一个字段。
现在我们就能够确保这个 car 具有期望中的 engine 了。我们将尝试这样做:
[<span color="#0080c0">Test</span>] <span color="#0000ff">public void</span> then_it_creates_a_car_with_an_eight_cylinder_engine() { _car.Engine.ShouldEqual(<span color="#0000ff">new</span> <span color="#0080c0">Engine</span> { Maker = <span color="#800000">"Acme"</span>, Type = <span color="#800000">"V8"</span> }); }
不幸的是,这个 spec 会失败。记住:在默认情况下,只有在两个对象都指向内存中的同一实例时,它们才是相等的。
因此,我们在这里不检查它们的相等性,而是使用 SpecsFor 中的 ShouldLookLike 扩展方法,它能够让我们检查两个对象看起来是否相同,即使它们并不指向内存中的同一个实例:
[<span color="#0080c0">Test</span>] <span color="#0000ff">public void</span> then_it_creates_a_car_with_an_eight_cylinder_engine() { _car.Engine.ShouldLookLike(() => <span color="#0000ff">new</span> <span color="#0080c0">Engine </span> { Maker = <span color="#800000">"Acme"</span>, Type = <span color="#800000">"V8"</span> }); }
通过使用这个 ShouldLookLike 方法,它只会检查我们所指定的属性。而 engine 中的其它属性都会被忽略。
最后,让我们确认 CarFactory 确实会调用 engine 工厂。要实现这一点,可以让 SpecsFor 再次为我们提供这个 mock 对象,随后使用 Moq 的 API 以验证 GetEngine 方法确实已被调用过了。
[<span color="#0080c0">Test</span>] <span color="#0000ff">public void</span> then_it_calls_the_engine_factory() { GetMockFor<<span color="#0080c0">IEngineFactory</span>>() .Verify(x => x.GetEngine(<span color="#800000">"V8"</span>)); }
这个 spec 略有些多余,因为我们已经确认在汽车中添加了正确类型的发动机,但这个 spec 所表现的是,我们不仅能够对 mock 进行配置,还能够验证对 mock 所进行的操作。
目前为止,我们依然只接触到了 SpecsFor 的表面。让我们看看一些对其行为进行扩展,让它服从你的需求的方式。
高级 SpecsFor 主题
由于 SpecsFor 中的几乎所有行为都可以被重写,因此你可以轻易地添加你所需的行为。比方说,你未必总是希望 SpecsFor 为你创建待测试系统的实例,有时你可能会希望能够自己进行创建,那么在这种情形下,你可以选择重写 InitializeClassUnderTest 方法:
<span color="#0000ff">protected override void</span> InitializeClassUnderTest() { SUT = <span color="#0000ff">new</span> <span color="#0080c0">CarFactory</span>(<span color="#0000ff">new</span> <span color="#0080c0">RealEngineFactory</span>()); }
你也同样可以对 SpecsFor 中使用的自动 mock 容器进行配置,如果你希望使用一个真实的、具体的类型,而不是实现了某个接口的 mock 对象,那么你可以这样做:
<span color="#0000ff">protected override void</span> ConfigureContainer(<span color="#0080c0">IContainer </span>container) { container.Configure(cfg => { cfg.For<<span color="#0080c0">IEngineFactory</span>>().Use<<span color="#0080c0">RealEngineFactory</span>>(); }); }
你也可以通过使用 ConfigureContainer 方法加载完成的应用程序注册表(如果你使用了 StructureMap 的话),这一点在你进行集成测试的时候会非常有用。
SpecsFor 中还存在一种配置系统,它允许你根据某些约定,在 spec 中加入自定义的行为。你可以通过创建一个派生自 SpecsForConfiguration 类的 NUnit SetUpFixture 类,定义你自己的约定。这一系统能够让你实现一些强大的功能,例如通过使用 Entity Framework,创建事务性的、隔离性的 spec。下面是一个简单的示例:
<span color="#008000">//The configuration class...</span> [<span color="#0080c0">SetUpFixture</span>] <span color="#0000ff">public class</span> <span color="#0080c0">SpecsForConfig</span> : <span color="#0080c0">SpecsForConfiguration</span> { <span color="#0000ff"> public</span> SpecsForConfig() { WhenTesting<<span color="#0080c0">INeedDatabase</span>>().EnrichWith<<span color="#0080c0">EFDatabaseCreator</span>>(); WhenTesting<<span color="#0080c0">INeedDatabase</span>>().EnrichWith<<span color="#0080c0">TransactionScopeWrapper</span>>(); WhenTesting<<span color="#0080c0">INeedDatabase</span>>().EnrichWith<<span color="#0080c0">EFContextFactory</span>>(); } } <span color="#008000">//The "marker" interface...</span> <span color="#0000ff">public interface</span> <span color="#0080c0">INeedDatabase</span> : <span color="#0080c0">ISpecs</span> { <span color="#0080c0">AppDbContext</span> Database { <span color="#0000ff">get</span>; <span color="#0000ff">set</span>; } } <span color="#008000">//The class that creates the EF database for your specs to use</span> <span color="#0000ff">public class</span> <span color="#0080c0">EFDatabaseCreator </span>: <span color="#0080c0">Behavior</span><<span color="#0080c0">INeedDatabase</span>> { <span color="#0000ff">private static bool</span> _isInitialized; <span color="#0000ff">public override void</span> SpecInit(<span color="#0080c0">INeedDatabase</span> instance) { <span color="#0000ff"> if</span> (_isInitialized) <span color="#0000ff">return</span>; <span color="#0080c0">Directory</span>.GetCurrentDirectory()); <span color="#0000ff">var</span> strategy = <span color="#0000ff">new</span> <span color="#0080c0">MigrateDatabaseToLatestVersion </span> <<span color="#0080c0">AppDbContext</span>,<span color="#0080c0"> Configuration</span>>(); <span color="#0080c0"> Database</span>.SetInitializer(strategy); <span color="#0000ff"> using</span> (<span color="#0000ff">var</span> context = <span color="#0000ff">new</span> <span color="#0080c0">AppDbContext</span>()) { context.Database.Initialize(force: <span color="#0000ff">true</span>); } _isInitialized = <span color="#0000ff">true</span>; } }
<span color="#008000">//The class that ensures all your operations are wrapped in a transaction that's //rolled back after each spec executes.</span> <span color="#0000ff">public class</span> <span color="#0080c0">TransactionScopeWrapper</span> : <span color="#0080c0">Behavior</span><<span color="#0080c0">INeedDatabase</span>> { <span color="#0000ff">private</span> <span color="#0080c0">TransactionScope</span> _scope; <span color="#0000ff">public override void </span>SpecInit(<span color="#0080c0">INeedDatabase</span> instance) { _scope = <span color="#0000ff">new</span> <span color="#0080c0">TransactionScope</span>(<span color="#0080c0">TransactionScopeAsyncFlowOption</span>.Enabled); } <span color="#0000ff">public override void</span> AfterSpec(<span color="#0080c0">INeedDatabase</span> instance) { _scope.Dispose(); } } <span color="#008000">//And the factory that provides an instance of your EF context to your specs</span> <span color="#0000ff">public class</span> <span color="#0080c0">EFContextFactory</span> : <span color="#0080c0">Behavior</span><<span color="#0080c0">INeedDatabase</span>> { <span color="#0000ff">public override void</span> SpecInit(<span color="#0080c0">INeedDatabase</span> instance) { instance.Database = <span color="#0000ff">new</span> <span color="#0080c0">AppDbContext</span>(); instance.MockContainer.Configure(cfg => cfg.For<<span color="#0080c0">AppDbContext</span>>().Use(instance.Database)); } <span color="#0000ff"> public override void</span> AfterSpec(<span color="#0080c0">INeedDatabase</span> instance) { instance.Database.Dispose(); } }
有了这些约定之后,你就轻易地实现在编写的 spec 中充分使用你的应用程序逻辑,乃至对数据库的操作。在下面这个示例中,该 spec 将通过某个 MVC 控制器,列出所有的任务对象:
<span color="#0000ff">public class</span> <span color="#0080c0">when_getting_a_list_of_tasks</span> : <span color="#0080c0">SpecsFor</span><<span color="#0080c0">HomeController</span>>, <span color="#0080c0">INeedDatabase</span> { <span color="#0000ff"> public</span> <span color="#0080c0">AppDbContext</span> Database { <span color="#0000ff">get</span>; <span color="#0000ff">set</span>; } <span color="#0000ff">protected override void</span> Given() { <span color="#0000ff">for </span>(<span color="#0000ff">var </span>i = 0; i < 5; i++) { Database.Tasks.Add(<span color="#0000ff">new</span> <span color="#0080c0">Task</span> { Id = <span color="#0080c0">Guid</span>.NewGuid(), Title = <span color="#800000">"Task "</span> + i, Description = <span color="#800000">"Dummy task "</span> + i }); } Database.SaveChanges(); } <span color="#0000ff">private</span> <span color="#0080c0">ActionResult</span> _result; <span color="#0000ff">protected override void</span> When() { _result = SUT.Index(); } [<span color="#0080c0">Test</span>] <span color="#0000ff">public void</span> then_it_returns_tasks() { _result.ShouldRenderDefaultView() .WithModelType<<span color="#0080c0">TaskSummaryViewModel</span>[]>() .Length.ShouldEqual(5); } }
这个示例还表现出了 SpecsFor 中我们尚未接触到的另一面:就是 SpecsFor
SpecsFor辅助类库
与传统的 ASP.NET WebForms 相比,ASP.NET MVC 一个常常挂的嘴边的优点就在于它的可测试性。但是,如果你在编写针对 ASP.NET MVC 应用程序的 spec 方面投入过大量的时间,你大概也会承认,这种可测试性与理想中的水平还有很大的差距,尤其是在你测试的对象不仅仅是某些简单的控制器行为时表现得更为明显。如果要对 action filter 和 HTML 辅助方法进行测试,你必须创建大量的模拟对象或假(fake)对象,并以正确的方式组合在一起。如果这些对象没有被完美地组合起来,那么你很可能会遇到 NullReferenceException 或其它类似的问题。
SpecsFor
俗话说得好,“百闻不如一见”,我相信对于代码来说也是一样的。以下的示例展示了在不使用 SpecsFor
[<span color="#0080c0">Test</span>] <span color="#0000ff">public void</span> then_it_says_hello_to_the_user() { <span color="#0000ff">var</span> viewResult = _result.ShouldBeType<<span color="#0080c0">ViewResult</span>>(); <span color="#0000ff">var</span> model = viewResult.Model.ShouldBeType<<span color="#0080c0">SayHelloViewModel</span>>(); model.ShouldLookLike(<span color="#0000ff">new</span> <span color="#0080c0">SayHelloViewModel</span> { Name =<span color="#800000"> "John Doe"</span> }); }
下面这个 spec 具有相同的功能,但由于使用了 SpecsFor
[<span color="#0080c0">Test</span>] <span color="#0000ff">public void</span> then_it_says_hello_to_the_user() { _result.ShouldRenderDefaultView() .WithModelLike(<span color="#0000ff">new</span> <span color="#0080c0">SayHelloViewModel</span> { Name = <span color="#800000">"John Doe"</span> }); }
在不使用 SpecsFor
[<span color="#0080c0">Test</span>] <span color="#0000ff">public void</span> then_it_redirects_to_the_say_hello_action() { <span color="#0000ff">var </span>redirectResult = _result.ShouldBeType<<span color="#0080c0">RedirectToRouteResult</span>>(); redirectResult.RouteValues[<span color="#800000">"controller"</span>].ShouldEqual(<span color="#800000">"Home"</span>); redirectResult.RouteValues[<span color="#800000">"action"</span>].ShouldEqual(<span color="#800000">"SayHello"</span>); redirectResult.RouteValues[<span color="#800000">"name"</span>].ShouldEqual(<span color="#800000">"Jane Doe"</span>); }
而当你使用了 SpecsFor
[<span color="#0080c0">Test</span>] <span color="#0000ff">public void</span> then_it_redirects_to_the_say_hello_action() { _result.ShouldRedirectTo<<span color="#0080c0">HomeController</span>>( c => c.SayHello(<span color="#800000">"Jane Doe"</span>)); }
想要测试 action filter 吗?祝你好运!因为你必须创建一个 ActionExecutingContext。下面的 spec 展示了你需要编写的代码,可以明显看出编写这段代码的过程是非常痛苦难熬的:
<span color="#0000ff">private</span> <span color="#0080c0">ActionExecutingContext</span> _filterContext; <span color="#0000ff">protected override void</span> When() { <span color="#0000ff"> var</span> httpContext = <span color="#0000ff">new </span><span color="#0080c0">Mock</span><<span color="#0080c0">HttpContextBase</span>>().Object; <span color="#0000ff"> var</span> controllerContext = <span color="#0000ff">new </span><span color="#0080c0">ControllerContext</span>(httpContext, <span color="#0000ff">new RouteData</span>(), <span color="#0000ff">new</span> <span color="#0080c0">Mock</span><<span color="#0080c0">ControllerBase</span>>().Object); <span color="#0000ff"> var</span> reflectedActionDescriptor = <span color="#0000ff">new</span> <span color="#0080c0">ReflectedActionDescriptor</span>(<span color="#0000ff">typeof</span>(<span color="#0080c0">ControllerBase</span>).GetMethods()[0], <span color="#800000">"Test"</span>, <span color="#0000ff">new</span> <span color="#0080c0">ReflectedControllerDescriptor</span>(<span color="#0000ff">typeof</span>(<span color="#0080c0">ControllerBase</span>))); _filterContext = <span color="#0000ff">new </span><span color="#0080c0">ActionExecutingContext</span>(controllerContext, reflectedActionDescriptor, <span color="#0000ff">new</span> <span color="#0080c0">Dictionary</span><<span color="#0000ff">string</span>, <span color="#0000ff">object</span>>()); SUT.OnActionExecuting(_filterContext); }
而在使用 SpecsFor
<span color="#0000ff">private</span> <span color="#0080c0">FakeActionExecutingContext</span> _filterContext; <span color="#0000ff">protected override void</span> When() { _filterContext = <span color="#0000ff">new</span> <span color="#0080c0">FakeActionExecutingContext</span>(); SUT.OnActionExecuting(_filterContext); }
在 SpecsFor
之后的步骤
即使你还没有使用SpecsFor 的打算,我也希望你至少会对SpecsFor 产生些好奇心。如果你打算尝试它的功能,那么上手使用它是非常简单的,只要选择一个合适的测试执行器,创建一个新的类库项目,并添加所需的 NuGet 包即可。如果你在进行 ASP.NET MVC 方面的开发,你可以通过使用 SpecsFor
如果你打算深入学习SpecsFor 的功能,那么有许多资源会对你有所帮助。官方文档中包含了额外的多个示例,并且为常见的测试挑战提供了多种解决方案。如果你希望系统地进行学习,也可以在 Pluralsight 找到 SpecsFor 的教程。而如果你更愿意通过研究源代码的方式进行学习,那么可以直接查看 GitHub 上的代码库,其中包含了 SpecsFor 的所有代码以及相关的实用工具。
关于作者
Matt Honeycutt是一位 ASP.NET web 应用方面的软件架构师,尤其精通 ASP.NET MVC 开发。他非常热衷于使用测试驱动开发技术,并以此创建了 SpecsFor 和 SpecsFor.Mvc 框架。他曾在多个价值数百万的软件项目中担任首席开发者的角色,并且非常享受为各种困难的问题寻找优雅的解决方案。他是一位计算机科学学科的博士,在 Pluralsight.com 上提供各种教程,并且在各种研究性杂志上发表论文,同时也进行各种技术的演讲,其内容覆盖了数据挖掘、机器学习和人机交互等等。他的博客地址是 trycatchfail.com 。此外,他也经常在于田纳西州举行的各种软件会议上进行演讲。
查看英文原文: Intro to .NET Unit & Integration Testing with SpecsFor
评论