这些实践为开发团队既带来了难得的机遇,也带来了独特的挑战,但所有这些机遇和挑战都是为了帮助从业人员建立“根据设计进行测试”的思路。
利用 BDD 技术,您可使用业务语言来编写自动化测试,同时还可保持与已实现系统的连接。
SpecFlow 可帮助您在 Visual Studio 中编写和执行规范,而 WatiN 可用于驱动浏览器进行自动化的端到端系统测试。
在介绍“测试优先”方法的基本内容后,我将介绍 SpecFlow 和 WatiN,并向您演示如何将这些工具与 MSTest 结合使用来为您的项目实现 BDD 的示例。
自动化测试简史
因此,实现任何新功能的第一步就是通过一个失败测试来描述您的期望(参见图 1)。

图 1 测试驱动开发的周期
最后,许多对此感兴趣的开发人员遇到了其组织内部对这项工作的重重阻力,要么是因为“测试”这个词暗示这项职能属于另一个团队,或是因为“TDD 产生了太多额外的代码并减缓了项目进度”这个错误的观念。
Steve Freeman 和 Nat Pryce 在他们的著作“Growing Object-Oriented Software, Guided by Tests”(Addison-Wesley Professional, 2009) 中指出,“传统的”TDD 缺少真正的“测试优先”开发的某些优点:
我们也看到了,有些具有高品质和经过严格单元测试的代码的项目并非从任何位置都可以调用,或者这些项目无法与系统的其余部分集成,因而必须重写。”
尽管这些实践就本身而言仍属于 TDD 的范畴,但却促使 North 采用一种更加侧重分析的观点来看待测试,并创造了术语“行为驱动开发”以概括这种转换。
完成编写后,开发人员将使用规范及其现有的 TDD 过程来实现足量的生产代码,从而得到一个通过测试的方案(参见图 2)。

图 2 行为驱动开发的周期
从何处开始设计
我仍然编写单元测试,但是 BDD 鼓励采用由外而内的方法,该方法首先要提供所要实现的功能的完整说明。
在传统的 TDD 实践中,您可以在图 3 中编写测试,以便演练 CustomersController 的 Create 方法。
图 3 针对创建客户的单元测试
- [TestMethod]
-
public void PostCreateShouldSaveCustomerAndReturnDetailsView() {
- var customersController = new CustomersController();
- var customer = new Customer {
- Name = "Hugo Reyes",
- Email = "hreyes@dharmainitiative.com",
- Phone = "720-123-5477"
- };
-
- var result = customersController.Create(customer) as ViewResult;
-
- Assert.IsNotNull(result);
- Assert.AreEqual("Details", result.ViewName);
- Assert.IsInstanceOfType(result.ViewData.Model, typeof(Customer));
-
- customer = result.ViewData.Model as Customer;
- Assert.IsNotNull(customer);
- Assert.IsTrue(customer.Id > 0);
- }
-
然后,我将该方案用作针对实现所需代码的各个单元的指南,以使此方案通过。
图 4 功能级别规范
- Feature: Create a new customer
- In order to improve customer service and visibility
- As a site administrator
- I want to be able to create, view and manage customer records
-
- Scenario: Create a basic customer record
- Given I am logged into the site as an administrator
- When I click the "Create New Customer" link
- And I enter the following information
- | Field | Value |
- | Name | Hugo Reyes |
- | Email | hreyes@dharmainitiative.com |
- | Phone | 720-123-5477 |
- And I click the "Create" button
- Then I should see the following details on the screen:
- | Value |
- | Hugo Reyes |
- | hreyes@dharmainitiative.com |
- | 720-123-5477 |
-
对于图 3 中的 CustomersController,一旦到达功能中的合适步骤,我就会在实现使该步骤通过测试所需的控制器逻辑之前立即编写此测试。
BDD 和自动化测试
cukes.info),它是一个基于 Rub 的测试工具,强调创建以“特定于域的业务可读语言”编写的功能级别的验收测试。
图 4 中的功能是此类语法的一个示例。
在 Cucumber 中,系统会对用户可读的功能文件进行解析,并将每个方案步骤与 Ruby 代码(演练相关系统的公共接口并确定该步骤是成功还是失败)进行匹配。
github.com/richardlawrence/Cuke4Nuke) 等 BDD 测试工具,您可首先在过程中创建可执行规范,在扩建功能时利用这些规范,并在最后记录那些与您的开发和测试进程直接关联的功能。
SpecFlow 和 WatiN 入门
我更愿意我的单元测试项目只包含单元测试(控制器测试、存储库测试等等),这样,我就还可以为我的 SpecFlow 测试创建一个 AcceptanceTests 测试。
添加 AcceptanceTests 项目并添加对 TechTalk.SpecFlow 程序集的引用后,请使用 SpecFlow 在安装时创建的“添加”|“新建项目”模板添加一个新功能,并将其命名为 CreateCustomer.feature。
关联的 .cs 文件中的代码代表该测试装置,即每次运行您的测试套件时实际执行的代码。
您只需向测试项目中添加一个 app.config 文件并添加以下元素即可:
<configSections>
<section name="specFlow"
type="TechTalk.SpecFlow.Configuration.ConfigurationSectionHandler, TechTalk.SpecFlow"/>
</configSections>
<specFlow>
<unitTestProvider name="MsTest" />
</specFlow>
首个验收测试
将 CreateCustomer.feature 文件中的默认文本替换为图 4 中的文本。
SpecFlow 需要此部分来自动生成测试,但是内容本身不能用于这些测试。
每个方案用于在关联的 .feature.cs 文件中生成一个测试方法(如图 5 所示),且方案中的每个步骤会传递到 SpecFlow 测试运行程序,该运行程序会将步骤的一个基于 RegEx 的匹配项执行到名为“步骤定义”文件的 SpecFlow 文件中的一个条目。
图 5 由 SpecFlow 生成的测试方法
- public virtual void CreateABasicCustomerRecord() {
- TechTalk.SpecFlow.ScenarioInfo scenarioInfo =
- new TechTalk.SpecFlow.ScenarioInfo(
- "Create a basic customer record", ((string[])(null)));
-
- this.ScenarioSetup(scenarioInfo);
- testRunner.Given(
- "I am logged into the site as an administrator");
- testRunner.When("I click the \"Create New Customer\" link");
-
- TechTalk.SpecFlow.Table table1 =
- new TechTalk.SpecFlow.Table(new string[] {
- "Field", "Value"});
- table1.AddRow(new string[] {
- "Name", "Hugo Reyesv"});
- table1.AddRow(new string[] {
- "Email", "hreyes@dharmainitiative.com"});
- table1.AddRow(new string[] {
- "Phone", "720-123-5477"});
-
- testRunner.And("I enter the following information",
- ((string)(null)), table1);
- testRunner.And("I click the \"Create\" button");
-
- TechTalk.SpecFlow.Table table2 =
- new TechTalk.SpecFlow.Table(new string[] {
- "Value"});
- table2.AddRow(new string[] {
- "Hugo Reyes"});
- table2.AddRow(new string[] {
- "hreyes@dharmainitiative.com"});
- table2.AddRow(new string[] {
- "720-123-5477"});
- testRunner.Then("I should see the following details on screen:",
- ((string)(null)), table2);
- testRunner.CollectScenarioErrors();
- }
-
请留意实际 .feature 文件中的异常报告方式,该方式与隐藏代码文件中的异常报告方式相反。

图 6 SpecFlow 找不到步骤定义
如果未找到匹配的步骤,则 SpecFlow 将使用您的功能文件生成您的步骤定义文件中所需的代码,您可复制并使用这些代码,以开始实现这些步骤。
您将注意到,每个方法都使用 SpecFlow 特性进行了修饰,该特性将方法指定为 Given、When 或 Then 步骤,并提供用于将方法与功能文件中某个步骤匹配的 RegEx。
集成 WatiN 以进行浏览器测试
因为我要构建一个 ASP.NET MVC 应用程序,所以我可以使用许多工具,这些工具有助于编写 Web 浏览器的脚本以与网站进行交互。
watin.sourceforge.net 中下载 WatiN,并将对 WatiN.Core 的引用添加到您的验收测试项目,以方便使用。
为了处理此问题,我通常创建一个 WebBrowser 静态类作为 AcceptanceTests 项目的一部分,并利用该类来处理 WatiN IE 对象和 ScenarioContext(SpecFlow 将其用来存储方案中各步骤之间的状态):
- public static class WebBrowser {
- public static IE Current {
- get {
- if (!ScenarioContext.Current.ContainsKey("browser"))
- ScenarioContext.Current["browser"] = new IE();
- return ScenarioContext.Current["browser"] as IE;
- }
- }
- }
-
在 CreateCustomer.cs 中需要实现的第一个步骤是 Given 步骤,该步骤通过让用户以管理员身份登录到网站来开始测试:
利用 WatiN,您可拥有自己的测试驱动并让它与浏览器交互,以实现此步骤。
当我再次运行测试时,将自动打开一个 Internet Explorer 窗口,当 WatiN 与网站交互(单击链接并输入文本)时,我可以观察工作中的 WatiN(参见图 7)。

图 7 带有 WatiN 的 Autopilot 上的浏览器
您可使用以下代码实现该步骤:
- [When("I click the \" (.*)\" link")]
-
public void WhenIClickALinkNamed(string linkName) {
- var link = WebBrowser.Link(Find.ByText(linkName));
-
- if (!link.Exists)
- Assert.Fail(string.Format(
- "Could not find {0} link on the page", linkName));
-
- link.Click();
- }
-
只需向主页添加一个带有该文本的链接,下个步骤就会通过。
功能中每个步骤的间隔的作用类似于实现的虚拟绑定程序,鼓励您仅实现通过步骤所必需的功能。
为了简便起见,现在让我们实现剩下的步骤(参见图 8)。
图 8 步骤定义中剩下的步骤
- [When(@"I enter the following information")]
-
public void WhenIEnterTheFollowingInformation(Table table) {
- foreach (var tableRow in table.Rows) {
- var field = WebBrowser.TextField(
- Find.ByName(tableRow["Field"]));
-
- if (!field.Exists)
- Assert.Fail(string.Format(
- "Could not find {0} field on the page", field));
- field.TypeText(tableRow["Value"]);
- }
- }
-
- [When("I click the \"(.*)\" button")]
-
public void WhenIClickAButtonWithValue(string buttonValue) {
- var button = WebBrowser.Button(Find.ByValue(buttonValue));
-
- if (!button.Exists)
- Assert.Fail(string.Format(
- "Could not find {0} button on the page", buttonValue));
-
- button.Click();
- }
-
- [Then(@"I should see the following details on the screen:")]
-
public void ThenIShouldSeeTheFollowingDetailsOnTheScreen(
- Table table) {
- foreach (var tableRow in table.Rows) {
- var value = tableRow["Value"];
-
- Assert.IsTrue(WebBrowser.ContainsText(value),
- string.Format(
- "Could not find text {0} on the page", value));
- }
- }
-
现在我需要新的代码,这意味着我的步骤要从 BDD 的外部循环进入到 TDD 的内部循环,如图 2 所示。
第一步是创建一个失败的单元测试。
将单元测试写入实现步骤
具体地说,您要创建 Controller 的一个新实例,调用其 Create 方法,并确保您反过来可接收到合适的“视图”和“模型”:
- [TestMethod]
-
public void GetCreateShouldReturnCustomerView() {
- var customersController = new CustomersController();
- var result = customersController.Create() as ViewResult;
-
- Assert.AreEqual("Create", result.ViewName);
- Assert.IsInstanceOfType(
- result.ViewData.Model, typeof(Customer));
- }
-
而如果您完成了 Create 方法,则测试将立即通过:
- public ActionResult Create() {
- return View("Create", new Customer());
- }
-
如果您按照功能指示,将其随合适的字段一起添加,则您向完整的功能又迈进了一步。
用于实现此创建功能的由外而内的过程如图 9 所示。

图 9 方案到单元测试过程
这些相同的步骤通常会在此过程中重复出现,循环访问这些步骤的速度随着时间的推移将大大加快,尤其是当您在 AcceptanceTests 项目中实现帮助程序步骤(单击链接和按钮,填写表单等等)和着手测试每个方案中的关键功能时。
您现在可以猜到下面将发生什么事:测试将因为您尚不具有保存客户记录所需的逻辑而失败。
在添加接受客户对象以允许编译此测试的空白 Create 方法后,您将看到测试失败,随后请按以下方式完成 Create 方法:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(Customer customer) {
_repository.Create(customer);
return View("Details", customer);
}
当您需要访问任意协作对象,而该对象不存在或未提供您需要的功能时,则应该遵循您对 Feature 和 Controller 遵循的相同单元测试循环。
最后,在多次 TDD 循环和子循环后,您现在具有了通过测试的功能,它证明您系统中存在的一些端到端的功能合适。
您现已通过验收测试和一组完整的单元测试(用于确保系统进行扩展以添加新功能时新功能可以继续工作)实现了一组端到端功能。
关于重构的说明
当您在链中从通过单元测试移回到通过验收测试时,应遵循相同的过程,关注重构的机会,并重定义每个功能以及随后的所有功能的实现。
这就让主要步骤定义文件变得简单明了并针对您指定的唯一方案。
由于功能设计演变成为单元设计,因此要确保在编写测试时将您的功能考虑在内,还要确保按照不连续的步骤或任务调整测试。
我希望,无论您使用什么工具集,对 BDD 的研究都能够增加自己的软件开发实践的价值和关注度。
@BrandonSatrom 与他取得联系。
感谢以下技术专家对本文的审阅:Paul Rayner 和 Clark Sell