hans.hu

夫天地者,万物之逆旅也;光阴者,百代之过客也。而浮生若梦,为欢几何?古人秉烛夜游,良有以也。况阳春召我以烟景,大块假我以文章。

行为驱动开发之道

用Context/Specification风格编写单元测试,可以使用一种更加自然的方式反映出用户故事、客户需求。BDD更为注重从需求的角度将测试用例分为若干关注点Concerns,在类的层次上把Context, Action和Observations抽象到不同的方法中,从而可以很好的应用Arrange, Act, Assert模式进行单元测试的编写。

The core principles of BDD are:

  • Expressing behaviour in terms that show the value to the system actors
  • Expressing behaviours / scenarios in a format that clearly separates the context, the action and the observations

为了阐述方便,先看下面这个TDD风格的测试用例(上篇文章中的测试用例):

[Test]
public void TestRetrieveOrders() {
    // arrange
    var mockView = MockRepository.GenerateMock<IOrdersView>();
    var mockRepository = MockRepository.GenerateStub<IRepository<Order>>();

    List<Order> defaultOrders = new List<Order> { new Order("moq"), new Order("RhinoMock") };

    mockRepository.Stub(ir => ir.FindAll()).Return(defaultOrders);
    var presenter = new OrdersPresenter(mockView, mockRepository);
    // act
    presenter.OnInit();
    // assert
    mockView.AssertWasCalled(v => v.Orders = defaultOrders);
}

这是平时很常见的书写测试用例的传统方法,下面我将使用介绍的BDD框架进行重写,以实现Context/Specifications风格的单元测试。将上述测试代码以Context/Specifications方式表示出来,如下所示: 

Concern: Orders retrieve

Context: when initializing the presentation

Observation: should retrieve all the orders

SpecUnit.Net

基于.NET单元测试框架NUnit提供了BDD风格的语法。简单来说就是将初始化、执行代码部分抽象出来封装进ContextSpecification,然后在子类具体实现其中的Context, Because方法(Template Method),从而在一个测试类中很好的实现了Context, Action和Observation的分离:

展开

现在我们用SpecUnit来实现BDD风格的单元测试:

[Concern("Orders retrieve")]
public class when_initializing_the_presentation : ContextSpecification
{
    protected IOrdersView _view;
    protected IRepository<Order> _repository;
    protected OrdersPresenter _presenter;
    // mocking orders
    protected readonly List<Order> DefaultOrders = new List<Order> { new Order("moq"), new Order("RhinoMock") };
    // arrange
    protected override void Context() {
        _view = MockRepository.GenerateMock<IOrdersView>();
        _repository = MockRepository.GenerateStub<IRepository<Order>>();
        _repository.Stub(ir => ir.FindAll()).Return(DefaultOrders);
        _presenter = new OrdersPresenter(_view, _repository);
    }      
    // act
    protected override void Because() {
        _presenter.OnInit();
    }
    // assert
    [Observation]
    public void should_retrieve_all_the_orders() {
        _view.AssertWasCalled(v => v.Orders = DefaultOrders);
    }
}

其中Concern标示出本次测试的关注点,class标示出Context,表示了我们的一个测试用例;Because包含的是我们需要进行测试的行为,是具体的执行逻辑,而用Observation标记出来的方法代表了Specifications,表示我们期望得到的结果;另外,我们可以看到上述例子的类名、方法名都是自解释的,可以很容易明白所需要测试的内容。

在实际开发过程中会经常碰到多个Context重用相同的行为测试,显然可以通过继承的方式以达到行为重用的目的。 

此外SpecUnit.Net自带一个工具可以使用它来生成测试报告页面,直观的看到最后的测试结果:SpecUnit.Report.exe <assembly name>

specunit

Machine.Specifications(MSpec)

Aaron Jensen受到SpecUnit与RSpec的灵感启发而开发出来的。MSpec利用委托和匿名方法定义了以下几个委托:

Establish:

  • 进行Context(Class)里的初始化工作
  • 一个Context(Class)里至多只有一个Establish
  • 运行于Because之前

Because:

  • 需要测试的行为
  • 一个Context(Class)里只能有一个Because

It:

  • 测试期望得到的结果
  • 一个Context(Class)里可以有多个It委托
  • 运行在Because之后

Behaves_like:

  • 封装好的、可重用的行为

Cleanup:

  • Context(Class)里执行TearDown的工作
  • 运行于It委托之后

这些委托是我们编写MSpec测试代码时经常需要用到的:

namespace Machine.Specifications
{
  public delegate void Establish();

  public delegate void Because();

  public delegate void It();
  public delegate void Behaves_like<TBehavior>();

  public delegate void Cleanup();
}

上述测试例子的相应MSpec写法如下:

[Subject("Orders retrieve")]
public class when_initializing_the_presentation
{
    protected static IOrdersView _view;
    protected static IRepository<Order> _repository;
    protected static OrdersPresenter _presenter;
    // mocking orders
    protected static readonly List<Order> DefaultOrders = new List<Order> { new Order("moq"), new Order("RhinoMock") };
    // arrange
    Establish context = () => {
        _view = MockRepository.GenerateMock<IOrdersView>();
        _repository = MockRepository.GenerateStub<IRepository<Order>>();
        _repository.Stub(ir => ir.FindAll()).Return(DefaultOrders);
        _presenter = new OrdersPresenter(_view, _repository);  
    };
    // act
    Because of = ()=> _presenter.OnInit();
    // assert
    It should_retrieve_all_the_orders = () => _view.AssertWasCalled(v => v.Orders = DefaultOrders);
}

注意到这个例子,由于Context中的匿名方法需要访问_view, _repository等字段,所以其中的所有字段必须都是static。

为了可以将同样的行为应用到多个Context里,MSpec提供了[Behaviors]以达到重用的目的:

[Behaviors]
public class DateTimeParsingBehavior
{
  protected static DateTime ParsedDate;

  It should_parse_the_expected_date = () => ParsedDate.ShouldEqual(new DateTime(2009, 1, 21));
}

这样我们就可以在多个Context里使用该Behavior:

展开

同样,Machine.Specifications.Console.exe可以生成最后的测试报告页面。

xUnit BDD Extensions

随着使用xUnit测试框架的流行,有许多人希望可以基于xUnit构建自己的BDD单元测试用例,其中xUnit.Samples中就有很不错的例子,大家有兴趣可以下载看看,这里就不再详细解释。

Conclusion:

在.NET世界里常见的BDD框架还有:NBehave, NSpec等,目的都是以贴近自然语言的方式描述软件系统的行为过程。与TDD相比,BDD更侧重于从客户的角度来表达需求,方便需求方与实现方的交流与沟通。而从开发人员角度看,BDD相比TDD在组织测试代码的结构上有很大不同。

下面列出SpecUnit与MSpec的主要特点:

  TDD MSpec SpecUnit
[Category] [Category] [Subject] [Concern]
Arrange SetUp/TestFixtureSetUp Establish Context
Act Run code under test Because Because
Assert [Test] It [Observation]

References:

  1. machine.specifications - GitHub
  2. specunit-net - Google Code
  3. An Evolution of Test-Specification Styles – My Journey to MSpec
  4. Behaviors with MSpec
  5. Starting with BDD vs. starting with TDD
  6. Test Driven Requirements with MSpec

posted on 2010-06-18 16:22  hans.hu  阅读(2858)  评论(0编辑  收藏  举报

导航