使用编码招式(Coding Katas)、BDD和VS2010项目模板:第II部分

Jamie Phillips撰写了一系列文章,展示他如何结合编码招式、行为驱动开发以及项目模板,以提高他自己的开发实践能力,这一系列文章由3部分组成,这是第 2部分。这个部分Jamie将给读者介绍行为驱动开发(BDD),同时他会解释BDD如何提高单元测试的有效性。

我很优秀,但我能变得更好吗?

第II部分:行为驱动开发(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

This entry was posted in Agile, IDE. Bookmark the permalink.

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s