Jamie Phillips撰写了一系列文章,展示他如何结合编码招式、行为驱动开发以及项目模板,以提高他自己的开发实践能力,这一系列文章由3部分组成,这是第 2部分。这个部分Jamie将给读者介绍行为驱动开发(BDD),同时他会解释BDD如何提高单元测试的有效性。
测试驱动开发能克服开发团队遇到的许多问题:开发团队经常在代码实现之后才创建单元测试。使用测试驱动开发,在实现代码时,就要仔细考虑测试,并建立起测试。行为驱动开发则更进一步,通过使用自然语言,直接把单元测试(以及测试用例)同需求联系起来。那么这会带来什么?简而言之——团队所有成员都能理解测试用例以及单元测试;从需求分析师到测试人员到开发人员。
第I部分探讨了编码招式,接着让我们继续我在BDD中的发现之旅。完成保龄球招式几天后,我开始意识到我可以把重点放到实际的编码风格以及如何最好地去重构代码上,而不是问题本身。这就是BDD部分出现的地方。尽管我曾听说过BDD,也阅读过一些资料,但我还未曾有机会去实际使用它;就像我之前提到过的,我的许多单元测试是基于产品代码的,在产品代码中启用“实验性”的概念是不合时宜的。因此我在实践编程招式时,总是去回想David和Ben在会上是怎么做的,当我意识到David使用MSpec(Machine Specification——一个.NET的上下文/需求规格框架)就是引入了BDD,那就是我需要的。David为了在他的单元测试中使用BDD的格式,他使用了MSpec程序集,这种方式严重依赖ReSharper,MSpec是作为ReSharper插件使用的。出于我的经验,以及参与构建系统的经历,我坚持自己的立场,那就是无论我在我的机器上能做什么,在构建机器上也要能做。以ReSharper插件方式使用实际上不是一个合适的选择,因为在没有安装ReSharper的机器上就没法那么做了。
行为驱动开发是一种敏捷软件的开发技术,通过需求分析师、软件测试人员和软件开发人员的紧密合作,将附带测试用例的用例与单元测试紧紧连接在一起。
通常,编写完业务需求后,团队成员会进行深入的探讨,建立起具体的用例,由这些用例驱动测试用例、单元测试和最终代码的实现。通常认为BDD比TDD(测试驱动开发)更进一步,它扩展了测试先行的想法,在编码实现前,预期的结果就已经定义好,并容易理解。
行为驱动开发的本质是想让开发人员、测试人员以及非技术人员或者业务人员可以一起协作,通过使用自然语言一起参与软件的设计,从需求定义到测试,再到实现。这在单元测试层面有巨大的影响,不仅涉及到如何编写测试代码,也牵涉到测试类和方法的命名规范。看看下面这个测试类和测试方法是如何实现的:
[TestClass]
public class ItemLoadTests
{
[TestMethod]
public void TestLoadNullCustomer()
{// Arrange
// Create the stub instance
INorthwindContext context = MockRepository.GenerateStub<INorthwindContext>();
//IObjectSet<Customer> customers = new MockEntitySets.CustomerSet();
IObjectSet<Customer> customers = TestHelper.CreateCustomerList().AsObjectSet();
// declare the dummy ID we will use as the first parameter
const string customerId = "500";
// declare the dummy instance we are going to use
Customer loadedCustomer;
// Explicitly state how the stubs should behave
context.Stub(stub => stub.Customers).Return(customers);
// Create a real instance of the CustomerManager that we want to put under test
Managers.CustomerManager manager = new Managers.CustomerManager(context);
// Act
manager.Load(customerId, out loadedCustomer);
// Assert
context.AssertWasCalled(stub => { var temp = stub.Customers; });
// Check the expected nature of the dummy intance
Assert.IsNull(loadedCustomer);
}
}
你会注意到单元测试多数是以AAA的形式编写的(Arrange准备、Act执行、Assert断言),同时测试的方法名对编写它的开发人员/测试人员都非常清楚——别忘了,我们是在测试载入Null客户时会发生什么。
顺便说一句,在这个例子中我使用了RhinoMocks,它是一个Mock的框架,用于创建我的EntityFramework Context接口INorthwindContext的Mock实例,不要把它与稍后在BDD中使用的Context混淆了。
从BDD的角度出发,我们扪心自问,这确实是这个功能的意图吗?也许为这个特殊场景编写的用例更像是这样的:
在Northwind客户管理的上下文中,当使用系统中不存在的客户ID加载客户细节时,应该返回一个空实例。
前面为Null客户所做的测试想要去证明这个用例(但它可能是虚构的),这做的不错。不幸的是,漫不经心的观察者会忽略它的语法和上下文。
抓住下面相同的实例,并从用例的角度出发来驱动测试,你就会得到完全不为同的情形。首先要建立一个上下文基类,可以在后续场景中使用它,继承自Eric Lee编写的ContextSpecification类,它是专门为了在MSTest中使用BDD编写的。
/// <summary> /// Base Context class for CustomerManager Testing
/// </summary> public class CustomerManagerContext : ContextSpecification { protected INorthwindContext _nwContext; protected IObjectSet<Customer> _customers; protected string _customerId; protected Customer _loadedCustomer; protected Managers.CustomerManager _manager; /// <summary> /// Prepare the base context to be used by child classes
/// </summary> protected override void Context() { // Create the stub instance _nwContext = MockRepository.GenerateStub<INorthwindContext>(); _customers = TestHelper.CreateCustomerList().AsObjectSet(); // Create a real instance of the CustomerManager that we want to put under test
_manager = new Managers.CustomerManager(_nwContext); }
下面一部分代码是实际的测试类(继承了上面的CustomerManagerContext类),实现了一些辅助方法和测试方法:
/// <summary>
/// Test class for CustomerManager Context
/// </summary>
[TestClass]
public class when_trying_to_load_an_employee_using_a_non_existent_id : CustomerManagerContext
{
/// <summary>
/// The "Given some initial context" method
/// </summary>
protected override void Context()
{
base.Context();
_customerId = "500";
// Explicitly state how the stubs should behave
_nwContext.Stub(stub => stub.Customers).Return(_customers);
}
/// <summary>
/// The "When an event occurs" method
/// </summary>
protected override void BecauseOf()
{
_manager.Load(_customerId, out _loadedCustomer);
}
/// <summary>
/// The "then ensure some outcome" method.
/// </summary>
[TestMethod]
public void the_employee_instance_should_be_null()
{
_nwContext.AssertWasCalled(stub => { var temp = stub.Customers; });
// Check the expected nature of the dummy intance
_loadedCustomer.ShouldEqual(null);
}
}
你马上会发现,类和方法的命名规范跟之前的例子不同,删除掉下划线(_)就变成人可以阅读的结果,尤其当你将它们像下面这样比较时:
好的,命名规范不是唯一的不同……尽管对于重写方法,可能会多一点开销,但让编写测试的人专注于正在发生的事情是很重要的。
Context方法类似于原先测试中的Arrange,但这里我们单独考虑它——确保那是我们将要做的所有事情。
BecauseOf方法类似于原先测试中的Act,这里我们再次看到,它被分隔成单独的区域,以确保我们专注于测试对象的因果关系——比如,因为我们做了一些事情,所以我们应该得到一个结果。
最后,实际的MSTest TestMethod本身就是结果——如果你喜欢的话,就是“应该怎样”的格式;它类似于之前单元测试中的Assert。因此,从单元测试的角度来看,BDD利用了TDD,并且进一步推动它,将它与我们关心的用例联系了起来。
如果我们回到先前实践的保龄球Kata,我们的测试方法是下面这种格式(准备Arrange——执行Act——断言Assert):
/// <summary> /// Given that we are playing bowling /// When I bowl all gutter balls /// Then my score should be 0
/// </summary> [TestMethod] public void Bowl_all_gutter_balls() { // Arrange
// Given that we are playing bowling Game game = new Game(); // Act
// when I bowl all gutter balls for (int i = 0; i < 10; i++) { game.roll(0); game.roll(0); } // Assert
// then my score should be 0 Assert.AreEqual(0, game.score()); }
现在测试方法是下面这种格式(BDD):
[TestClass] public class when_bowling_all_gutter_balls : GameContext { /// <summary> /// The "When an event occurs" method
/// </summary> protected override void BecauseOf() { for (int i = 0; i < 10; i++) { _game.Roll(0); _game.Roll(0); } } /// <summary> /// The "then ensure some outcome" method.
/// </summary> [TestMethod] public void the_score_should_equal_zero() { _game.Score().ShouldEqual(0); } }
原先的测试结果是这样的:
现在的测试结果是这样的:
的确,这里的例子基于非常简单的用例,但却进一步阐明了一种观点:越是复杂的用例,它的测试会出现问题的情况就越明显。因此我们可以进一步划分这个用例(没有双关语意的)
下周Jamie Phillips会做一个总结,展示如何使用VS2010的项目模板来消除反复建立测试用例和项目的工作。
查看英文原文:Using Coding Katas, BDD and VS2010 Project Templates: Part 2