行为驱动开发之道
用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>
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] |