对验收测试、Gherkin及Spec Flow的介绍

http://www.infoq.com/cn/articles/Spec-Flow
对验收测试、Gherkin及Spec Flow的介绍

验收测试或功能测试是验证系统是否满足需求的一种测试。这些测试作为黑盒测试的一种,与其内部具体执行无关。验收测试只是用来验证系统是否符合某一需求。

现在我们一起看下面这个关于网页登录功能的需求:

Feature: Login
In order to access my account
As a user of the website
I want to log into the website

Scenario: Logging in with valid credentials
Given I am at the login page
When I fill in the following form
| field | value |
| Username | xtrumanx |
| Password | P@55w0Rd |
And I click the login button
Then I should be at the home page

其可读性非常强,是吧?以上的详细需求是通过Gherkin语言来描述的。Gherkin是一种领域特定语言,它允许我们在不解释具体执行细节的情况下,详细描述应用应该如何执行。以上详细需求的大部分内容是由自由文字组成;只有几个特定的Gherkin关键字:Feature、Scenario、Given、When、And和Then,其他的都是自由文字,并且主要记录了功能特性是如何被使用的。

Gherkin是一种基于行的编程语言,场景中的每一行(Line)就是一个步骤(Step)。“Logging in with valid credentials”场景中的第一个步骤是“Given I am at the login page”。该步骤需要一个具体步骤定义,这样我们的测试执行者(test runner)才能知道如何去完成该步骤。Spec Flow中的步骤定义其实就是一个带有变量的方法,而该变量包含有该步骤的具体文本。所有步骤定义方法都需包含于一个含有Binding属性的类中。

[Binding]
class LoginStepDefinitions
{
[Given(“I am at the login page”)]
public void GivenIAmAtTheLoginPage()
{
// TODO
}
}

上面的类和方法名都是随意的。真正重要的是应用到类和方法中的变量。如果没有它们,Spec Flow就无法确认步骤定义方法和具体步骤的绑定关系。

现在就剩下步骤定义执行了。而这时候就该WatiN和Nunit上场了。

WatiN是一个基于浏览器的自动化工具。我们将利用它来打开一个IE实例,浏览URL,填充表格,点击按钮或链接等。与此同时,我们将利用NUnit来断言我们的期望。尽管如此,WatiN和NUnit并非是必需的。也可以使用Selenium进行浏览器自动化;而事实上,任何单元测试框架都可用来断言,甚至可以使用Windows自带的应用自动化库,比如:White,然后为相应的Windows Forms或WPF应用编写自动化验收测试。

现在我们就来尝试为一个真实应用创建实际的验收测试。针对本文,我们将使用该实例程序。读者可以从这里的repository获取一份。该repository还包含完整的验收测试工程,但是我还是建议通过执行本文剩下的内容来创建自己的验收测试代码。
先决条件

Spec Flow为所有支持的第三方测试运行器授权以运行真正繁重的验收测试。正如前面所提的,我们将使用NUnit执行测试和WatiN自动化浏览器。以下就是如何使用WatiN自动化浏览器为WatiN执行谷歌查询的例子(来自WaitN网站)。

[Test]
public void SearchForWatiNOnGoogle()
{
using (var browser = new IE(“http://www.google.com”))
{
browser.TextField(Find.ByName(“q”)).TypeText(“WatiN”);
browser.Button(Find.ByName(“btnG”)).Click();

Assert.IsTrue(browser.ContainsText(“WatiN”));
}
}

上面的测试创建了一个新IE实例,然后将Google的URL传给构造器,然后由构造器让浏览器跳转到Google页面。随后寻找命名为“q”的文本框。该文本框就是你要输入具体查询内容的地方。找到该文本框后,输入“WatiN”。紧接着,查找命名为“btnG”的按钮,然后点击它。最后,由一个断言来确定页面上存在有“WatiN”(可以是页面上任何地方)。

以上的代码为我们快速展示了通过WatiN自动化常规任务是多么的简单,这些任务可以是在浏览器上执行例如填写文本框、点击按钮等动作。

接下来,就可以在Visual Studio中为你的验收测试创建新的类库工程。当你下载完NUnit和WatiN后,你将需要添加必需的DLL到你的验收测试工程中。从你的NUnit下载中添加一个nunit.framework.dll引用到你的验收测试工程。对于WatiN,你则需要添加两个DLL引用到你的验收测试工程:Interop.SHDocVw.dll和WatiN.Core.dll。

值得一提的是,你可以通过NuGet获取NUnit和WatiN这两个项目。这两个都很容易在NuGet中找到,并能自动添加到你的工程中。如果你已经在你的项目中使用NuGe了,大可以通过它下载这两个项目。

从网站上获取一份Spec Flow,然后安装到你的系统中。跟NUnit和WatiN不同,你需要在系统中安装Spec Flow,而不是简单的拷贝Dll文件。Spec Flow自带有某些工具,每次往项目中添加一个特性文件,它都会创建相应的隐藏代码文件。另外,在你编辑特性文件时,它还带有一些语法高亮和其他调整。

成功安装Spec Flow后,检查安装目录(默认为Program Files)。里面有一堆DLL文件,但是你只需要添加该引用到你的工程中:TechTalk.SpecFlow.dll。
建立验收测试工程

在我们进一步编写验收测试之前,我们需要建立我们的验收测试工程。我们将在工程中添加几个文件夹以便使项目更有条理。

Features

所有的说明都放在这个文件夹下。

StepDefinitions

所有情景步骤的步骤定义将放在这个文件夹下

StepHelpers

创建Spec Flow特性文件

在Features文件夹中添加一个新的名字为Login.feature的Spec Flow特性文件。该文件带有针对场景中新增特性的规格说明。可以将其删除,然后添加以下文本。

Feature: Login

Feature在Gherkin中是关键字。它需要在每个feature文件中出现一次,紧接着它是一个冒号和其特性名。然后你可以通过任意行的自由文字来描述该特性。为了保持简易性,Gherkin的创建者建议用户尽量将文档保持最短,并遵循以下格式:

In order to realize a named business value
As an explicit system actor
I want to gain some beneficial outcome which furthers the goal

个人看来,我更倾向于跳过上述描述,像登录这样能自我描述的特性,如果你尝试依据某一特定格式将其描述的话,反而让人困惑;有的时候很难区分出第一行中的“named business value”和第三行中你应描述的“beneficial outcome”之间的不同。需要记住的是该部分是自由文字,你可以任意描述。接下来就让我们先跳过这一段,开始编写情景。

Feature: Login
Scenario: Logging in with valid credentials

跟Feature一样,Scenario也是Gherkin的一个关键字,其后面紧接着一个冒号和其命名。跟Feature不同的是,scenario不能在一行中就完成,它需要由各个步骤一起来完成scenario。我们可以想一想:我们需要做什么通过有效凭证来成功登录?

填写登录表单
点击登录按钮

但是且慢,在我们能够填写登录表单前,我们需要打开有着登录表单的页面。之后,我们需要检查我们是否成功登录。可以假设在登录后,我们将被重定向到主页,从而意味着我们已经成功登录。

现在我们的scenario有了前提条件(比如:我们必须在登录页面)和后置条件(比如:我们在主页)。Gherkin中,前提条件需从关键字Given开始,而后置条件需要由关键字When开始。

Feature: Login

Scenario: Logging in with valid credentials
Given I am at the ‘Login’ page
When I fill in the following form
| field | value |
| Username | testuser |
| Password | testpass |
And I click the ‘Login’ button
Then I should be at the ‘Home’ page

是否注意到它的类表结构?Spec Flow会自动将第一行斜体化,该行在管道限制行中,并由管道开始。第一行为表头,紧接着各行中的每列将指代表头中定义的任意文本。比如,Password就是field在第二行的内容,而testuser则是value列在第一行的值。

同时,也应注意到由And开始的步骤。And关键字可以使用于任一步骤之后,并将被自动认为与前一步骤属于同一类型。如果And步骤紧接着Given步骤, 那么该步骤也被认为是一个Given步骤。在上述例子中,该And步骤被认为是When步骤。When步骤不用做pre或post条件,但是scenario需要这一部分用于进一步执行。

创建第一个步骤定义

现在我们完成了对Login特性的定义,但是我们的test runner还不知道如何执行该特性情景中的每一个步骤。我们需要为Login特性情景中的4个步骤一一定义。为了完成这个,我们将在Step目录中创建一个类,并将其命名为LoginSteps。为了让Spec Flow知道该类含有步骤定义,我们给该类绑定下Binding属性。该Binding属性属于TechTalk.SpecFlow命名空间的一部分。

using TechTalk.SpecFlow;

[Binding]
class LoginSteps
{
}

接下来,我们需要给每个步骤创建一个方法。该方法将告诉Spec Flow如何执行每一步骤。现在我们就只为第一个步骤进行具体步骤定义:“Given I am at the ‘Login’page”。

[Given(“I am at the ‘Login’ page”)
public void GivenIAmAtTheLoginPage()
{
// TODO
}

请注意到该方法带有一个属性。该属性将告诉Spec Flow此方法所指代的步骤,每个步骤都有相应的属性。任何一个feature文件中的任意一个以关键字“Given”开始的步骤,紧接着的文本是“I am at the ‘Login’page”的都将会与该方法配对。

现在我们需要为该步骤定义编写具体执行。我们需要告诉WatiN启动浏览器,然后访问程序登录页面。但是在这之前我们需要创建一个浏览器实例。我们也要确保scenario中剩余步骤也将使用该浏览器实例。为了保证scenario中所有步骤使用的是同一个浏览器实例,我们需要为浏览器对象创建一个实例,并将其保存于ScenarioContext字典中。ScenarioContext字典可以用于保存Scenario执行过程中的数据。我们还要创建一个名为WebBrowser的helper类,该类将保存scenario执行时的浏览器实例。

using TechTalk.SpecFlow;
using WatiN.Core;

static class WebBrowser
{
public static IE Current
{
get
{
if(!ScenarioContext.Current.ContainsKey(“browser”))
ScenarioContext.Current[“browser”] = new IE();

return ScenarioContext.Current[“browser”] as IE;
}
}
}

以上的Helper类含有Current属性,它将获取现有浏览器用于目前正在执行的scenario。如果它没在ScenarioContext字典中找到浏览器实例,它将创建一个新浏览器实例,然后将其添加到字典中。这样存在于字典中的浏览器实例又回来了。

最后,我们终于可以回去实现我们的步骤定义了。在示例应用中,登录页面存在于http://localhost:9876/authentication/login。我们将通过让当前Scenario浏览器实例定位到该URL以执行我们的步骤定义。我们可以通过浏览器实例回到主页,然后点击登录链接来执行我们的步骤定义,而这在现在看来是最简单的。然后,我们将重构我们的步骤定义,这样我们只要有一个步骤定义就能解决应用中所有页面跳转问题。

[Given(“I am at the ‘Login’ page”)
public void GivenIAmAtTheLoginPage()
{
// Make sure to add the namespace the WebBrowser class is inside
WebBrowser.Current.GoTo(“http://localhost:9876/authentication/login”);
}

结尾

现在我们已经为尝试执行验收测试做好准备了。虽然我们还没完成所有的步骤定义,但是我们想在进一步深入前,确保各方面都已经正确衔接上。在执行测试之前,我们要保证所有加到工程中的引用都已设置成了Copy Local。如果是通过NuGet添加的WatiN,Interop.SHDocVw DLL则默认将它的Embed Interop属性设置成True。这时,需要确保将Embed Interop属性设置成false,这样才能将它的Copy Local属性设置成True。

同时,我们也需要在单线程的Apartment State中运行NUnit,不然就不能自动化IE浏览器。之所以选择IE,而非Firefox是因为Firefox持续更新它的主要版本,而这会不断破坏WatiN与Firefox之间的衔接。

设置NUnit的Apartment State需要使用到配置文件。往工程中添加一个app.config文件,并添加以下配置。

执行验收测试

Login.feature文件将有个名字为Login.feature.cs(或者.vb)的代码隐藏文件。该文件是含有TestFixture属性的类,这样NUnit才能知道需要测试哪个类。如果你的Visual Studio中已经安装了NUnit测试运行插件,比如:TestDriven.Net或ReSharper,那么你就可以使用Login.feature.cs文件来运行测试。如果你没有相应的测试插件,那么你可以打开NUnit自带的、位于应用目录下的测试运行器,然后将其指向由验收测试产生的配置文件中,这样它将定位Login.feature.cs中的测试设置。

在执行测试前,保证应用的服务器处于运行状态。然后运行程序,一个新的IE实例将被打开,并跳转到登陆页面。如果做到了这些,那就说明各个部分已经被正确配置,我们就可以继续完成剩余的步骤定义。如果IE没有打开,或没有跳转到登陆页面,那就要进一步调试了。
完成剩余的步骤定义

编写剩余步骤定义将会非常简单。你只需对WatiN API有一定了解,这样就可以配置WatiN以填写表单或查找按钮和链接,然后对其点击。

首先,我们尝试情景中的第二个步骤:“When I fill in the following Form”。需要在LoginSteps类中创建一个方法,并带有一个变量将其绑定到我们目前正在工作的步骤上。

[When(“I fill in the following form”)]
public void WhenIFillInTheFollowingForm(TechTalk.SpecFlow.Table table)
{
// TODO
}

注意传到该步骤定义中的参数。该Table对象包含有我们在情景中所描述的值。而WatiN在其命名空间中也有一个Table类,为了避免冲突,我对该Table对象采用了完全限定名。该Table对象将由行组成。行中的每列可以通过索引或列名来获取。列名由情景中表的第一行来定义。

[When(“I fill in the following form”)]
public void WhenIFillInTheFollowingForm(TechTalk.SpecFlow.Table table)
{
foreach(var row in table.Rows)
{
var textField = WebBrowser.Current.TextField(Find.ByName(row[“field”]));

if(!textField.Exists)
Assert.Fail(“Expected to find a text field with the name of ‘{0}’.”, row[“field”]);
textField.TypeText(row[“value”]);
}
}

是的,只需要5行,我们就完成了填写表单的步骤定义。我们首先循环表中的行,然后试图查找相应的带有命名属性的field文本,用来匹配当前行中field列值。如果没有匹配到相应的文本field,测试将会失败(但是会有相应信息被记录下来)。但是如果我们找到一个匹配的文本field,WatiN将会被指示输入在当前行所找到的列值。

接下来剩余的步骤定义也基本类似。以下就是我们下个步骤的代码:“And I click the ‘Login’button”。

[When(“I click the ‘Login’ button”)]
public void AndIClickTheLoginButton()
{
var loginButton = WebBrowser.Current.Button(Find.ByValue(“Login”));

if(!loginButton.Exists)
Assert.Fail(“Expected to find a button with the value of ‘Login’.”);

loginButton.Click();
}

而最后一步,我们则需要弄清如何去验证我们是否在主页上。我们可以检测其文件标题来看它是否满足我们的需要;或者检测URL来查看其是否与期望的主页URL匹配。这里我们将通过URL来检测。但需要记住的是步骤定义的执行会完全依赖于具体应用:如果我们大量使用Ajax进行页面切换,URL可能不会被更新,从而可能无法验证我们是否在正确页面上。

[Then(“I should be at the ‘Home’ page”)]
public void ThenIShouldBeAtTheHomePage()
{
var expectedURL = “http://localhost:9876/”;
var actualURL = WebBrowser.Current.Url;
Assert.AreEqual(expectedURL, actualURL);
}

现在重新执行验收测试,而这次它将执行所有步骤。如果成功了,那么恭喜你,你成功地通过Spec Flow编写了第一个验收测试。
重构测试代码

现在我们的步骤定义已经成功运行了,可是我们还是需要停下来回顾一下。我们已经有了针对登录按钮的步骤定义,有很大可能我们的应用中将有大量的按钮。我们是否为验收测试集中的每个按钮提供一个步骤定义呢?这些步骤定义中唯一一个需要修改的地方就是WatiN在某页面上寻找期望按钮所对应的具体文本,以下就是个例子:

[When(“I click the ‘Login’ button”)]
public void AndIClickTheLoginButton()
{
var loginButton = WebBrowser.Current.Button(Find.ByValue(“Login”));

if(!loginButton.Exists)
Assert.Fail(“Expected to find a button with the value of ‘Login’.”);

loginButton.Click();
}

[When(“I click the ‘Register’ button”)]
public void AndIClickTheRegisterButton()
{
var registerButton = WebBrowser.Current.Button(Find.ByValue(“Register”));

if(!registerButton.Exists)
Assert.Fail(“Expected to find a button with the value of ‘Register’.”);

registerButton.Click();
}

幸运的是,Spec Flow为该问题提供了解决方案。你可以使用正则表达式,该表达式必须是传递给绑定属性的字符串,这样我们就将不同步骤绑定到同一个步骤定义。任何由该正则表达式捕获的文本都可以当作一个参数传递给步骤定义。具体例子如下:

[When(“I click the ‘(.*)’ button”)]
public void AndIClickAButton(string buttonText)
{
var button = WebBrowser.Current.Button(Find.ByValue(buttonText));

if(!button.Exists)
Assert.Fail(“Expected to find a button with the value of ‘{0}’.”, buttonText);

button.Click();
}

该步骤定义可以是以下任意一种:

点击‘登陆’按钮
点击‘注册’按钮
点击‘任意文本’按钮

你可以使用该技巧来让所有步骤定义可重用。让所有的步骤定义都具有可重用性是个非常好的想法,我们应该避免编写只有某一特定情景才可以用的步骤定义。可重用性允许你写出的Scenario能立即使用,因为你已经拥有一个可以重复使用的步骤定义。但是也没必要在需要前就把步骤定义编写出来,在需要时编写就好了。大多数情况下,你的测试所需要做的基本上是同样的事情:跳转到一个页面,填写表单,点击之类的。只有在极少数情况下,你才需要为一个情景编写特殊步骤定义,比如验证jQuery UI日历是否在点击一个需要日期值的文本框时弹出,这样不寻常的例子时候才需要。
对于具体如何重构你的步骤定义,我将其作为练习留给你,确保每个都具有可重用性。之后,你就可以编写新的特性文件,而因为所需的步骤定义都已经准备好了,它们都可以立刻工作。当然,你将需要更多的步骤定义来自动化不同行为(比如:点击一个链接),你也需要进一步改善当前步骤定义,以便在不同情景中也能使用。你将注意到我们的表单填充步骤定义中处理的表单只带有文本框。如果你想勾选复选框,通过value或ID寻找按钮,或其它与表单相关操作,那你就需要在表单填充步骤定义中添加相应的逻辑。
总结

总之,以上就是使用Spec Flow编写验收测试之旅。尽管如此,Spec Flow依然有更多特性需要你自己去发现。比如:Spec Flow还有个跟踪机制,因此你能跟踪某个指定特性在特定特性,情景或步骤前后是如何执行某些代码。对于以下情况,它就会起到非常大的作用,比如:如果你需要在完成含有登陆功能的场景后的登出;测试前准备数据库,或只是简单地在每个测试结束后关闭浏览器窗口。

随着测试不断被创建,你会发现完成验收测试集所需的时间也会随之增加。但是你的测试执行得越快,你就能越快得到回馈,并找到问题所在。你需要尽可能快地执行你的测试。方法之一就是将你的测试并行化,而非按顺序执行,所有测试将一起执行,而且将更快结束。但是Spec Flow并没有提供并行处理功能,因此需要从别处想办法。如果你使用NUnit,你可以查看PNUnit看其是否满足你的并行需求。

Gherkin,我们曾经是使用DSL来编写我们的详细需求,它被设计作为衔接技术人员与非技术利益相关者之间的桥梁,以便他们在某一特定特性应该如何运行上能够达成一致。有的团队甚至有非技术利益相关者参与到使用Given-When-Then语句来编写实际详细需求。可以想象到,培训大家如何做到这点并不太难,但是其结果却大大不同。

也有一部分人更进一步地使用Spec Flow。他们在开始所有新特性开发时,首先就是编写详细需求,接着单元测试,最后才是真正的编码。然后他们通过常规的TDD red-green-refactor循环通过单元测试,最后通过验收测试。如果你已经在实践TDD,打算为项目编写验收测试,那你应该尝试下验收测试驱动开发。

无论你是通过什么途径,你都需确保它对你、还有你的工程都要行之有效。如果你还处于建模状态,并不断修改你的应用,持续更新验收测试可能会造成一定时间拖延。什么时候应该着手编写验收测试是你的决定,只有你能保证它将发生。
关于作者

Mustafa Saeed Haji Ali 现居于Hargeisa,Somaliland。作为一名软件开发工程师,他通常利用ASP.Net MVC来工作。Mustafa热衷于测试和使用JaveScript框架,比如:KnockoutJS,AngularJS和SignalR。在传播最佳实践上,Mustafa也有极大的热情。

参考英文原文: Writing Automated Acceptance Tests with Spec Flow

感谢陈菲对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ)或者腾讯微博(@InfoQ)关注我们,并与我们的编辑和其他读者朋友交流。

This entry was posted in UT. 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