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