在 ASP.NET MVC 应用程序中使用 SpecFlow 进行 BDD






4.92/5 (30投票s)
我将通过在 ASP.NET MVC 应用程序中使用 BDD 来注册新用户的故事进行演练。
目录
- 引言
- 什么是 BDD?
- BDD 的必备工具
- SpecFlow 概述
- 初始设置
- 一览结果
- 故事:注册新用户
- 功能:注册新用户
- 场景 1 - 浏览注册页面
- 场景 2 - 成功注册后,用户应重定向到主页
- 场景 3 - 如果用户名缺失,注册应返回错误
- 参考文献
- 历史
引言
我一直在做 TDD(测试驱动开发/设计)一段时间了,并且发现它非常有用。编写测试总是一件令人痛苦的事情,如果你没有正确地进行。TDD 的问题似乎更侧重于编写测试,而开发人员可能更关注系统的设计和行为。
问题本身不在于 TDD,而在于思维模式。它关乎于将我们的思想调整到正确的位置。因此,BDD,也称为行为驱动开发,应运而生。这些不仅仅是术语上的改变,而是关于我们编写测试或 BDD 语境下的规范方式的改变。废话不多说,让我们深入探讨这个未知的领域。
什么是 BDD?
首先。我不想复制已经发布的内容。因此,了解一些理论的最佳场所是 Wiki:http://en.wikipedia.org/wiki/Behavior_Driven_Development [^]。
但为了完整起见,这里有一个简短的总结。BDD 是一种敏捷软件开发技术,它鼓励软件项目中的开发人员、QA 和非技术或业务参与者之间的协作。它更多地关注业务规范而不是测试。您为故事编写规范,并验证规范是否按预期工作。BDD 开发的主要特点概述如下:
- 一个可测试的故事(它应该是适合迭代的最小单元)
- 标题应描述一个活动
- 叙述应包含角色、功能和好处
- 场景标题应说明不同之处
- 场景应以“给定”、“事件”和“结果”来描述
- “给定”应定义所有必需的上下文,并且仅此而已
- “事件”应描述功能
查看“参考文献”部分的“一篇有趣的读物”,以更详细地解释上述每一点。我们将使用 ASP.NET 2 MVC 应用程序(ASP.NET 1.0 MVC 也应该可以工作)附带的 Membership Provider 来编写我们将围绕“注册新用户”的网站故事。
但在此之前,让我们快速看一下我们将用于此示例故事的工具。
BDD 的必备工具
以下是我将在此演示中使用的一系列工具。请在继续或尝试之前设置好这些工具。下载是独立的,包含所有依赖项。但要获取 BDD 的代码模板,您必须安装 SpecFlow。
SpecFlow 概述
SpecFlow 是一个 .NET 的 BDD 库/框架,它提供了与Cucumber类似的功能。它允许以人类可读的 Gherkin 格式编写规范。有关 Gherkin 的更多信息,请参阅Gherkin 项目。
Gherkin 是 Cucumber 所理解的语言。它是一种业务可读的领域特定语言,允许您描述软件行为,而无需详细说明该行为是如何实现的。它只是一个用于描述给定系统所需功能的 DSL。这些功能按功能细分,每个功能都有多个场景。场景由三个步骤组成:GIVEN(给定)、WHEN(当)和 THEN(然后)(这似乎与 TDD 的 AAA(Arrange, Act, Assert)语法有些关系)。
有关 Gherkin 的更多信息,请参阅Gherkin 项目。
初始设置
- 下载并运行 SpecFlow 安装程序。
- 创建一个新的类库项目,并添加对 SpecFlow、Moq 和 NUnit Framework 的引用。
我们将类库项目命名为“SpecFlowDemo.Specs”,以确立我们正在为业务功能编写规范的心态。
一览结果
我们的目标是获得一份格式精美的规范报告。
故事:注册新用户
让我们快速看一下它的用户界面。
第一步是添加一个“SpecFlow”功能。我们将把所有功能都放在上述创建的 Spec 项目的 Features 文件夹中。feature 文件是我们定义规范的地方。它是一个简单的文本文件,带有一个自定义设计器,该设计器会生成样板代码。
让我们添加一个新功能。右键单击“Features”文件夹,选择“Add New”Item,然后选择“SpecFlowFeature”,如下图所示。
这将创建一个新文件“RegisterUser.feature”和一个设计器文件“RegisterUser.feature.cs”。该文件的默认内容如下所示。它是 Gherkin 格式。
Feature: Addition
In order to avoid silly mistakes
As a math idiot
I want to be told the sum of two numbers
@mytag
Scenario: Add two numbers
Given I have entered 50 into the calculator
And I have entered 70 into the calculator
When I press add
Then the result should be 120 on the screen
“RegisterUser.feature.cs”文件包含使用 NUnit(在此例中)自动创建规范的样板代码。此文件不应手动编辑。
上述模板说明了我们正在尝试做什么,在此例中是“加法”,然后有不同的场景来支持该功能。
每次保存此文件时,您都会调用一个自定义工具“SpecFlowSingleFileGenerator”。它会解析上述文件并根据所选的单元测试框架创建设计器文件。
与其解释上面的 feature,不如让我们深入研究我们用于“注册新用户”的第一个测试用例。
功能:注册新用户
Feature: Register a new User
In order to register a new User
As member of the site
So that they can log in to the site and use its features
我们在上述 feature 中概述了我们的基本需求。让我们看一下应用程序可能需要处理的与上述 feature 相关的不同场景。
场景 1 - 浏览注册页面
将以下场景键入/复制到 .feature 文件中。
Scenario: Browse Register page
When the user goes to the register user screen
Then the register user view should be displayed
编译 spec 项目。启动 NUnit GUI 并打开“SpecFlowDemo.Specs.dll”。首先,从 Tools->Settings->Test Loader->Assembly Reload 中更改以下设置。
确保选中了以下选项:
- 每次测试运行前重新加载
- 测试程序集更改时重新加载
- 重新运行上次运行的测试
这样做将在每次编译测试时自动执行它们。
现在,当您执行此测试时,您应该会看到以下屏幕。在 NUnit GUI 中点击“Text Ouput”选项卡。
您可以看到 SpecFlow 根据 feature 文件中指定的“Scenario”生成了两个“StepDefinitions”。
现在,在 Visual Studio->Your Spec Project->Add a New Class File 中。在我们的例子中,名称是“RegisterUserSteps.cs”。这将是您的 spec 类。
将这两个方法复制到此文件中,并删除行“ScenarioContext.Current.Pendin()
”。
我们第一个场景的完整源代码如下所示。有几次我会显示完整的源代码以便于理解,其余时间我将只显示必要的代码片段。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using TechTalk.SpecFlow;
using SpecFlowDemo.Controllers;
using SpecFlowDemo.Models;
using NUnit.Framework;
using System.Web.Mvc;
using Moq;
using System.Web.Security;
namespace SpecFlowDemo.Specs
{
[Binding]
public class RegisterUserSteps
{
ActionResult result;
AccountController controller;
[When(@"the user goes to the register user screen")]
public void WhenTheUserGoesToTheRegisterUserScreen()
{
controller = new AccountController();
result = controller.Register();
}
[Then(@"the register user view should be displayed")]
public void ThenTheRegisterUserViewShouldBeDisplayed()
{
Assert.IsInstanceOf<viewresult>(result);
Assert.IsEmpty(((ViewResult)result).ViewName);
Assert.AreEqual("Register",
controller.ViewData["Title"],
"Page title is wrong");
}
}
}
编译测试并在 NUnit 中运行它。您将看到以下失败。
错误的原因是,“Register
”方法使用了 MembershipService
,我们需要对其进行模拟。看看 AccountController
的 Register
方法。
public ActionResult Register()
{
ViewData["Title"] = "Register";
ViewData["PasswordLength"] = MembershipService.MinPasswordLength;
return View();
}
为了使其正常工作,我们需要在 AccountController
中添加一个重载构造函数,该构造函数将接收所需的依赖项。
public AccountController(IFormsAuthenticationService formsService,
IMembershipService memberService)
{
FormsService = formsService;
MembershipService = memberService;
}
这是修改后的测试以及 Moq 对象。
[Binding]
public class RegisterUserSteps
{
ActionResult result;
AccountController controller;
Mock<imembershipservice> memberService = new Mock<imembershipservice>();
Mock<iformsauthenticationservice> formsService =
new Mock<iformsauthenticationservice>();
[When(@"the user goes to the register user screen")]
public void WhenTheUserGoesToTheRegisterUserScreen()
{
controller = new AccountController(formsService.Object, memberService.Object);
result = controller.Register();
}
[Then(@"the register user view should be displayed")]
public void ThenTheRegisterUserViewShouldBeDisplayed()
{
Assert.IsInstanceOf<viewresult>(result);
Assert.IsEmpty(((ViewResult)result).ViewName);
Assert.AreEqual("Register",
controller.ViewData["Title"],
"Page title is wrong");
}
}
请注意,在上面的测试中,我们没有“Given”条件。尽管这是可选的。我们模拟了 Membership
和 FormsAuthenticationService
并将其传递给 AccountController
。这是测试结果。
现在我们有了一个通过的测试。
场景 2 - 成功注册后,用户应重定向到主页
Scenario: On Successful registration the user should be redirected to Home Page
Given The user has entered all the information
When He Clicks on Register button
Then He should be redirected to the home page
测试代码如下所示。
[Given(@"The user has entered all the information")]
public void GivenTheUserHasEnteredAllTheInformation()
{
registerModel = new RegisterModel
{
UserName = "user" + new Random(1000).NextDouble().ToString(),
Email = "test@dummy.com",
Password = "test123",
ConfirmPassword = "test123"
};
controller = new AccountController(formsService.Object, memberService.Object);
}
[When(@"He Clicks on Register button")]
public void WhenHeClicksOnRegisterButton()
{
result = controller.Register(registerModel);
}
[Then(@"He should be redirected to the home page")]
public void ThenHeShouldBeRedirectedToTheHomePage()
{
var expected = "Index";
Assert.IsNotNull(result);
Assert.IsInstanceOf<redirecttorouteresult>(result);
var tresults = result as RedirectToRouteResult;
Assert.AreEqual(expected, tresults.RouteValues["action"]);
}
场景 3 - 如果用户名缺失,注册应返回错误
Scenario: Register should return error if username is missing
Given The user has not entered the username
When click on Register
Then He should be shown the error message "Username is required"
测试代码如下所示。
[Given(@"The user has not entered the username")]
public void GivenTheUserHasNotEnteredTheUsername()
{
registerModel = new RegisterModel
{
UserName = string.Empty,
Email = "test@dummy.com",
Password = "test123",
ConfirmPassword = "test123"
};
controller = new AccountController(formsService.Object,
memberService.Object);
}
[When(@"click on Register")]
public void WhenClickOnRegister()
{
result = controller.Register(registerModel);
}
[Then(@"He should be shown the error message ""(.*)""")]
public void ThenHeShouldBeShownTheErrorMessageUsernameIsRequired(string errorMessage)
{
Assert.IsNotNull(result);
Assert.IsInstanceOf(result);
Assert.IsTrue(controller.ViewData.ModelState.ContainsKey("username"));
Assert.AreEqual(errorMessage,
controller.ViewData.ModelState["username"].Errors[0].ErrorMessage);
}
此测试用例中的一些值得关注的点:注意“Then”部分中的 (.*) 表达式。这允许您将参数传递给测试。在此例中,参数是“Username is required”,它从“feature”文件中传递。
请参阅源代码以查找完整的测试用例集。希望我能够触及这些优秀的课题。
最后一点,要获取 *HTML* 输出,请按照下图所示的步骤进行操作。
打开 NUnit GUI,然后转到 Tools->Save results as XML。为文件命名并将其保存在项目位置。然后,为 VS IDE 设置以下外部工具。
- 在 Visual Studio 中转到 Tools->External Tools 菜单。
- 点击 Add。
- 按照上图所示填写值。
要获取 *HTML* 报告,请点击“Tools”菜单中的“SpecFlow”。
这是我的 NUnit XML 文件的位置。
希望您觉得这很有用。我将更新它,提供更多改进和测试用例。有关 TDD 和 BDD 的基础知识,CodeProject 上有大量的文章可供参考。
参考文献
历史
- 2010 年 5 月 21 日 - 首次发布。