行为驱动开发与测试的目标:
行为驱动开发是一个软件工程的系列实践,能够帮助团队快速构建和交付更多价值和质量的软件产品。
总结一句话,把代码语言结合自然语言,让所有人都能看的懂的逻辑,让代码可读性最大化。
解决基本的问题:
公司往往在信任方面存在巨大的问题 - 客户不信任供应商,业务不信任开发人员,开发人员不相信测试人员,测试人员也不信任任何人。黄瓜给业务,开发人员和测试人员提供了一种方式,用简单的自然语言来协作和指定系统应该如何工作。我们已经看到,这些简单的Cucumber规范的对话是如何开始恢复过程的。
黄瓜使规范的直接自动化,这意味着任何人都可以看到,什么功能已经实现,什么没有。它还为开发团队提供了一个安全网络,以便在他们正在进行的更改已经破坏了任何现有功能的情况下获得及时反馈。它可以让你的测试人员做一些有趣而有创意的工作,而不是定期通过一个重复的手动回归包来运行。 黄瓜现在已经被成千上万的队伍使用,并以不同的方式从中受益。我们这些从一开始就被吸引到黄瓜身上的人本能地认识到,这不仅仅是一个测试自动化工具,这是一个协作工具。让我们清楚这一点 - 黄瓜可以用于测试自动化 - 但这不是它的创造者的意图,我们的一些设计选择反映了这一点。通过写这本书,我们希望不仅告诉你如何使用黄瓜,但如何使用它。 黄瓜最初是用Ruby编写的。多年来,它已经变得非常受欢迎,并已被移植到其他许多语言.
黄瓜社区充满了激烈的争论,我们花了好几个小时的时间让自己的想法在与其他用户的讨论中受到挑战和磨练。我们希望在本书中尽可能多地提炼出知识和经验。
在第一部分中,我们将带您了解为了使用黄瓜而需要了解的核心概念。新手读者将学习他们需要知道的一切起步和运行,读者已经与黄瓜应该得到大量有用的细节。 第二部分通过使用Cucumber开发一个新的应用程序的一个实际例子。当我们从零开始构建一个简单的应用程序时,您将与我们配对,让您有机会体验我们如何使用Cucumber构建软件并巩固您在第一部分中学到的知识。我们还将教您一些高级功能在一个例子的背景下更容易学习的黄瓜。 在第三部分中,您将学习在以前的工作示例中未涉及的情况下使用Cucumber的技术,以及如何更详细地了解如何配置Cucumber for Java。
Cucumber最初是由Ruby社区的成员创建的一个命令行工具。它已经被翻译成几种开发环境,包括Java,让我们更多的人可以享受它的好处。
运行Cucumber时,它会从纯语言文本文件(称为功能)中读取您的规范,检查它们以测试场景,并根据您的系统运行场景。
每个场景都是黄瓜工作的步骤列表。因此,黄瓜可以理解这些功能文件,他们必须遵循一些基本的语法规则。这套规则的名字是小黄瓜。
除了这些特性之外,您还给了Cucumber一个步骤定义,它将每个步骤的业务可读语言映射为代码(在本书中用Java编写),以执行步骤描述的任何操作。在一个成熟的测试套件中,步骤定义本身可能只是一行或两行代码,委托给支持代码库(特定于应用程序领域),知道如何在系统上执行常见任务。有时候可能会涉及使用自动化库(如浏览器自动化库Selenium)与系统本身进行交互。
如果步骤定义中的代码执行时没有错误,则Cucumber继续执行场景中的下一步。如果到达场景的末尾而没有任何引发错误的步骤,则表明该场景已经过去。
但是,如果场景中的任何步骤都失败,则Cucumber会将场景标记为失败,并转到下一个步骤。在场景运行的过程中,黄瓜会打印出结果,显示出正确的是什么,哪些不是。
这就是简而言之。Cucumber还有许多其他的优点,使它成为一个很好的选择:你可以用四十多种不同的语言编写你的规范,你可以使用标签来组织和分组你的场景,你可以很容易地与一个高质量自动化库来驱动几乎任何类型的应用程序。在阅读本书其余部分时,所有这些和更多内容将变得清晰。
黄瓜专门用来帮助业务利益相关者参与写作验收测试。 Cucumber中的每个测试用例都被称为场景,并且场景被分组为特性。
每个场景包含几个步骤。
黄瓜测试套件中面向业务的部分存储在特征文件中,必须根据语法规则(称为小黄瓜)编写,以便黄瓜可以读取它们。 在引擎盖下,步骤定义从面向业务的步骤语言转化为代码。
为了说明这些概念,在下一章中,我们将深入研究并构建一个非常简单的应用程序,使用Cucumber来推动开发。
对...我们开始吧!
了解我们的目标
我们的目标是编写一个Java库来计算超市的杂货成本。
有些人可能会称这是一个结帐。
我们有一个令人难以置信的视野,这个结账将有一天会为您提供强大的动力:传统的结账服务,您购物时随身携带的便携式扫描仪,甚至是使用手机上的相机扫描条形码的基于云的服务。
不过,我们是务实的商业人士,所以我们图书馆的第一个版本必须尽可能简单。
第一个版本将是一个Java库。这将需要两个输入:可用项目的价格和项目在结帐时被扫描的通知。
结帐将跟踪总成本。
所以,例如,如果可用项目的价格如下所示:
香蕉40c 苹果25c
而您在结帐时扫描的唯一项目是这样的: 1个香蕉 那么输出将是40c。
同样,如果您扫描多个项目: 3个苹果 那么输出将是75c。 你明白了。
创建一个功能
黄瓜测试分为功能。
我们使用这个名字,因为我们希望他们能够描述用户在使用我们的程序时能够享受的功能。
我们需要做的第一件事是创建一个目录,在这个目录中我们将存储我们的新程序以及我们将要为其编写的功能。
$ mkdir checkout
$cd checkout
接下来我们需要的是获得几个运行黄瓜的最低限度的JAR。
创建一个新的文件夹放置在:
$ mkdir jars
从公共Maven仓库[9]下载最新版本的JAR ,并将它们复制到jar文件夹中。
这足以让我们使用Java的Cucumber。
• cucumber-core
• cucumber-java
• cucumber-jvm-deps
• gherkin
我们将让黄瓜引导我们完成结账程序的开发,所以让我们立即从结帐文件夹中运行黄瓜开始:
$ java -cp "jars/*" cucumber.api.cli.Main -p pretty .
No features found at [.]
0 Scenarios
0 Steps
0m0.000s
创建步骤定义
不要过多地考虑它们的含义,我们只需从Cucumber的最后一个输出中复制并粘贴片段到一个新的Java文件中。让我们创建一个新的文件夹来保留我们的步骤定义:
$ MKDIR step_definitions
现在在step_definitions中创建一个名为CheckoutSteps.java的Java文件。只要它是一个Java文件,Cucumber就不介意你所说的,但这是一个很好的名字。在文本编辑器中打开它并输入以下类定义:
first_taste/04/step_definitions/CheckoutSteps.java
package step_definitions;
import cucumber.api.java.en.*;
import cucumber.api.PendingException;
public class CheckoutSteps {
}
现在粘贴这些片段:
package step_definitions;
import cucumber.api.java.en.*;
import cucumber.api.PendingException;
public class CheckoutSteps {
@Given("^the price of a "(.*?)" is (\d+)c$")
public void thePriceOfAIsC(String arg1, int arg2) throws Throwable {
//此处写一段代码,原来这句话上面变成具体行动
throw new PendingException();
}
@When("^I checkout (\d+) "(.*?)"$")
public void iCheckout(int arg1, String arg2) throws Throwable {
//此处写一段代码,原来这句话上面变成具体行动
throw new PendingException();
}
@Then("^the total price should be (\d+)c$")
public void theTotalPriceShouldBeC(int arg1) throws Throwable {
//此处写一段代码,原来这句话上面变成具体行动
throw new PendingException();
}
}
我们现在要做的就是运行Cucumber,这样它可以告诉我们接下来要做什么,但是在我们做之前,我们必须编译新的CheckoutSteps类并将其添加到我们的类路径中。这是我们通常在我们最喜欢的IDE中工作时不用打扰的工作,但是由于我们是通过命令行来完成的,我们现在编辑我们的cucumber shell文件,以便编译我们的Java代码以及调用黄瓜。
first_taste/04/cucumber
javac -cp "jars/*" step_definitions/CheckoutSteps.java
java -cp "jars/*:." cucumber.api.cli.Main -p pretty --snippets camelcase
-g step_definitions features
塞布说:类路径分隔符
Java类路径的语法取决于底层的操作系统。为* nix的操作系统的分离器是一个冒号(:),而适用于Windows操作系统的分隔符是分号(;)。
我们在本章中展示了黄瓜脚本,这是用于* nix的用户。您还可以在可下载的代码中找到cucumber.bat批处理文件,其中包含完全相同的操作,但格式化为Windows。
第1行编译我们刚刚创建的CheckoutSteps类。然后第2行调用Cucumber。Cucumber的调用有两个轻微的增加:
1、我们已经将当前目录“。”添加到类路径中。
2、 我们已经添加了-g step_definitions命令行参数来告诉Cucumber在哪里查找步骤定义,它需要将功能文件中的步骤“粘合”到checkout应用程序(我们还没有写入)。
现在,让我们再次执行./cucumber,看看接下来我们需要做什么:
Feature: Checkout
Scenario: Checkout a banana # checkout.feature:2
Given the price of a "banana" is 40c # CheckoutSteps.thePriceOfAIsC(String,int)
cucumber.api.PendingException: TODO: implement me
at step_definitions.CheckoutSteps.thePriceOfAIsC(CheckoutSteps.java:12)
at *.Given the price of a "banana" is 40c(checkout.feature:3)
When I checkout 1 "banana" # CheckoutSteps.iCheckout(int,String)
Then the total price should be 40c # CheckoutSteps.theTotalPriceShouldBeC(int)
1 Scenarios (1 pending)
3 Steps (2 skipped, 1 pending)
0m0.138s
cucumber.api.PendingException: TODO: implement me
at step_definitions.CheckoutSteps.thePriceOfAIsC(CheckoutSteps.java:12)
at *.Given the price of a "banana" is 40c(checkout.feature:3)
该场景已经从未定义到待定。这是个好消息,因为这意味着Cucumber现在正在运行第一步,但是正如它所做的那样,它触发了在我们复制粘贴的步骤定义代码中抛出新PendingException()的调用,这告诉Cucumber这个场景是还在进行中。
我们需要用一个真正的实现来替换这个异常。
请注意,黄瓜报告另外两个步骤跳过。一旦遇到失败或挂起的步骤,黄瓜将停止运行该方案,并跳过其余的步骤。
我们来实现第一步定义。
实施我们的第一步定义
我们已经决定,我们结帐的第一个版本将是一个类,它将价目表和购买的项目作为方法的参数。
所以,我们在定义香蕉价格的步骤中的工作是40c只是记住香蕉的价格。在step_definitions文件夹中,编辑CheckoutSteps.java文件,以便第一步的定义如下所示:
package step_definitions;
import cucumber.api.java.en.*;
import cucumber.api.PendingException;
public class CheckoutSteps {
@Given("^the price of a "(.*?)" is (\d+)c$")
public void thePriceOfAIsC(String name, int price) throws Throwable {
int bananaPrice = price;
}
太好了,那很容易。现在,我们又在哪里?那么,我们已经写了一些Java代码,所以我们需要编译CheckoutSteps类并通过运行./cucumber再次运行Cucumber。
好极了!我们的第一步通过!当然,这个场景仍然被标记为待定,因为我们还有其他两个步骤来实现,但是我们已经开始到了某个地方。
改变黄瓜的产量
每次运行黄瓜的输出内容时,都会分散注意力。让我们切换到使用进度插件来获得更有针对性的输出。编辑黄瓜,以便运行黄瓜的行如下所示:
现在,当您运行./cucumber时,您应该看到以下输出:
进度插件不是打印整个功能,而是在输出中打印了三个字符,每个步骤一个字符。第一个。字符表示通过的步骤。在P字指的第二步,因为我们知道,正在等待。最后的-字符意味着,在最后一步被跳过。黄瓜有几个不同的插件,产生不同格式的输出; 在本书的学习过程中,你将会学到更多的知识。
插件
黄瓜插件允许您自定义工具的行为。Cucumber附带的插件会生成各种输出格式,记录测试运行中发生的情况。有生成HTML报告的插件,为Jenkins等持续集成服务器生成JUnit XML的插件等等。
使用这个命令来查看可用的不同插件,并为自己尝试一些:
java -cp“jars / *”cucumber.api.cli.Main --help
注意:我们将后面更多地解释插件
这是一个有趣的小分流,但让我们回去工作。我们有待解决的问题!
测试我们的结帐类
要实现下一步,请编辑step_definitions / CheckoutSteps.java,以便第二步定义如下所示:
first_taste / 07 / step_definitions / CheckoutSteps.java
@When("^I checkout (\d+) "(.*?)"$")
public void iCheckout(int itemCount, String itemName) throws Throwable {
Checkout checkout = new Checkout();
checkout.add(itemCount, bananaPrice);
}
这段代码试图在我们的Checkout类的一个实例上调用一个add()方法,并传递它被购买的数量和价格。
这一次,当我们运行./cucumber,我们应该得到一个编译错误,因为我们还没有创建一个Checkout类:
我们的步骤是失败的,因为我们还没有一个结帐类使用。
你可能会认为我们编写并运行试图运行我们的Checkout类的代码有点奇怪,完全知道Checkout类还不存在。
我们故意这样做,因为我们要确保我们有一个完全正常的测试,然后才能开始解决问题。
有了这个纪律来做到这一点意味着我们可以相信我们的测试,因为我们已经看到他们失败了,这使我们相信,当测试通过,我们真的完成了。
这种温和的节奏是我们所谓的“ 外在”发展的重要组成部分,虽然起初看起来可能有些奇怪,但我们希望在整本书中告诉你它有很大的好处。
从外部进行工作的另一个好处是,我们有机会从用户的角度思考我们的checkout类的命令行界面,而没有付出努力去实现它。
在这个阶段,如果我们意识到有一些我们不喜欢的界面,我们很容易就可以改变它。
现在我们创建一个文件夹来保存我们的实现:
$ mkdir implementation
现在在实现中创建一个名为Checkout.java的Java文件。在文本编辑器中打开它并输入以下类定义:
我们需要将其添加到我们的脚本cucumber的编译步骤中,以及修改类路径以包含项目的根目录:
我们将Checkout类导入CheckoutSteps:
我们再次运行黄瓜:
我们仍然有一个语法错误。变量bananaPrice位于PriceOfAIsC()的本地,但是我们试图在iCheckout()中使用它。让我们把它变成一个实例变量,以便它可以被CheckoutSteps中的所有步骤定义共享:
我们现在有两个传递步骤和Checkout的框架实现。
添加一个断言
要使最后一步工作,请将step_definitions / CheckoutSteps.java中的最后一步定义更改为如下所示:
我们使用JUnit断言来检查功能中指定的预期总数是否与我们结帐中的总数相匹配。如果没有,JUnit会提出错误。
在编译之前,我们需要添加一个import语句到step_definitions / CheckoutSteps.java:
我们还需要下载最新的JUnit JAR [11],并把它放在jar文件夹中。
现在将一个total()的实现添加到Checkout:
当我们运行./cucumber时,会得到另一个编译错误:
我们在iCheckout()中创建的局部变量签出已经超出了我们在TotalPriceShouldBeC()稍后调用total()的时间。
我们需要做的是使其成为CheckoutSteps的实例变量:
这是我们在同一个Java类中实现的不同步骤定义之间共享信息的常用方法。稍后,你会看到其他方法来做到这一点,但这是一个简单的方法,你会经常使用。
现在当我们运行./cucumber时,我们有一个真正的失败测试:
棒!现在我们的测试没有完全正确的原因:它使用Checkout,检查总数,并告诉我们总体应该是什么样子。这是暂停休息的自然点。我们为这个版本做了很多努力:当我们回到这个代码时,Cucumber会告诉我们我们需要做些什么来让我们的程序工作。如果只有我们所有的要求都准备好了,像这样一个失败的测试,建设软件将是容易的!
尝试这个 你可以写一个实现Checkout.java,使场景通过?请记住,在这个阶段我们只有一个场景可以满足,所以你可能能够逃脱一个简单的解决方案。
我们将在下一部分向您展示我们的解决方案。
乔问:我觉得奇怪:你正在通过测试,但没有任何工作!
我们实现了一个使用Checkout类并传递的步骤,即使“类”只包含没有任何用处的方法实现。这里发生了什么?
请记住,一个步骤本身不是一个测试。测试是整个场景,直到其所有步骤都不会完成。当我们实现所有的步骤定义时,只有一种方法可以让整个场景通过,那就是建立一个可以总项目的结账!
当我们像这样在外面工作时,我们经常使用临时存根(如空的Checkout类)作为占位符,以获得稍后需要填写的详细信息。我们知道,我们不能永远离开这个空的文件,因为最终Cucumber会告诉我们回来,让它做一些有用的事情,以便让整个场景通过。
这个原则,故意做最小的有用的工作,测试将让我们摆脱,可能看起来很懒,但实际上这是一门学科。
它确保我们彻底地进行测试:如果测试不会促使我们写出正确的东西,那么我们需要一个更好的测试。
通过
所以,现在我们已经完成了黄瓜案例,现在是时候让这个案例推出一个解决方案。
有一个非常简单的解决方案可以使测试通过,但这不会让我们走的太远。无论如何,我们来试试吧,为了好玩:
尝试一下。最后你会看到情况通过:
万岁!那么这个解决方案有什么问题?毕竟,我们已经说过,我们想做一些测试让我们放弃的最小工作,对吧?
其实,这不是我们所说的。我们说我们要做的测试将让我们摆脱的最小有用的工作。我们在这里所做的事情可能已经让测试通过了,但是这不是很有用。
除了它现在还不能作为结账的功能之外,我们来看看我们用我们的smarty-pants单线解决方案测试时错过了什么: 我们没有使用任何一个输入。我们没有试图总结任何事情。
在晶莹透明:小团队的人性化方法 [Coc04]中,Alistair Cockburn主张在项目中尽可能早地构建一个步行骨架,以便尽量排除任何潜在的技术选择问题。
很明显,我们的结账非常简单,但是仍然值得考虑这个原则:为什么我们不建立更有用的东西来通过这个场景,并帮助我们更多地了解我们的计划实施?
如果你对这个论点不以为然,那就把它看作是一个重复的问题。我们在两个地方的硬编码值为40:一次在我们的情况下,一次在我们的结帐。在一个更复杂的系统中,这种重复可能会被忽略,我们会有一个很脆弱的情况。让我们强迫自己解决这个问题,使用Kent Beck所说的三角测试(Test Driven Development:By Example [Bec02])。
我们将使用名为“ 场景大纲”的新关键字为我们的功能添加另一个场景:
我们将场景转换为场景纲要,可以让我们使用表格指定多个场景。我们可以复制和粘贴整个场景,只是改变了值,但我们认为这是一个更可读的方式来表达例子,我们想给你一个Gherkin语法的可能性。现在看看输出结果如何:
从总结2个场景(1个失败,1个通过)我们可以看出,黄瓜运行了两个场景。当“黄瓜”运行场景大纲时,“ 示例”表中的每一行都展开为一个场景。
结果是40的第一个例子依然通过,但第二个例子是失败的。
现在用一个更现实的解决方案来重新实现我们的程序是非常有意义的:
首先我们创建一个实例变量runTotal,以跟踪运行总数。然后我们增加这个在添加。最后,我们回到了runningTotal的总方法。
试试看 两种情况都通过了吗?棒!你刚刚用黄瓜建立你的第一个程序。