行为驱动测试
为了自动执行.feature文件,您需要定义与模式相关联的脚本代码的步骤实现。每次在.feature文件中遇到一个步骤时,Squish都会尝试找到与步骤文本匹配的模式。如果找到匹配项,将执行关联的代码。
注意:如果您需要视频指导,Qt Academy上有40分钟的在线课程,关于Squish的行为驱动测试。
使用Step定义步骤实现
步骤实现是通过调用预定义的Step函数来定义的。简单的定义如下
@Step("user starts the addressbook application") def step(context): startApplication("addressbook")
Step("user starts the addressbook application", function(context) { startApplication("addressbook"); });
Step {user starts the addressbook application} {context} {
startApplication addressbook
}Step("user starts the addressbook application") do |context| Squish::startApplication("addressbook") end
use Squish::BDD; Step "user starts the addressbook application", sub { startApplication("addressbook"); };
在这个例子中,匹配模式“用户启动地址簿应用”的步骤将导致Squish执行
startApplication("addressbook")startApplication("addressbook");startApplication addressbook
Squish::startApplication("addressbook")
startApplication("addressbook");步骤文本
步骤文本,在从文本中剥离任何关键字(如Given)后,必须与模式完全匹配。匹配是从文本的开始和结束锚定的。
下表显示了哪些步骤文本会与“用户启动地址簿应用”模式匹配
| 步骤文本 | 模式匹配? |
|---|---|
| 当一个用户启动地址簿应用时 | 是 |
| 当一个用户启动地址簿应用时我们谈论 | 否 |
| Given 一个用户启动地址簿应用 | 是 |
| Given 我们假定一个用户启动地址簿应用 | 否 |
注意:忽略初始关键字,如Given或When,步骤文本必须与模式完全匹配。文本前面或后面不应有任何文本。
限制
通过Step定义步骤始终会成功,除非以下情况
- 给定的模式是空的或格式不正确——后者在使用正则表达式时可能发生。
- 对于给定的模式已存在一个已存在的定义。
- 对于脚本语言,其中
Step接受signature参数:签名至少应包含一个用于上下文的参数。
使用带有占位符的步骤模式
模式可以使用某些占位符从步骤文本中提取数据并将其传递给脚本代码。这允许使步骤定义更具可重用性,因为它避免了必须硬编码特定值。例如,这里是上述步骤定义的改进,避免了硬编码要启动的应用程序的名称
@Step("user starts the |word| application") def step(context, appName): startApplication(appName)
Step("user starts the |word| application", function(context, appName) { startApplication(appName); });
Step {user starts the |word| application} {context appName} {
startApplication $appName
}Step("user starts the |word| application") do |context, appName| Squish::startApplication(appName) end
use Squish::BDD; Step "user starts the |word| application", sub { my %context = %{shift()}; my $appName = shift(); startApplication($appName); };
应用程序名称addressbook不再硬编码。相反,|word|将匹配单词,并通过新的appName参数传递。
以下占位符可以使用
| 占位符 | 含义 |
|---|---|
|word| | 匹配单个单词。单词是长度为一或多个字符的字符,由字母、数字和下划线(_)字符组成。 |
|integer| | 匹配正或负整数值,即没有小数部分的数字。 |
|any| | 匹配一个或多个任意字符的序列。 |
使用正则表达式与步骤模式一起使用
有时,您可能会发现上述提到的占位符不足,需要更多控制哪些步骤文本部分被提取。这可以通过不将普通字符串传递给Step函数,而是一个正则表达式来实现。正则表达式功能更强大,但也需要更加小心——它们很容易变得对读者来说有点难以理解。
正则表达式不仅允许更精细地控制要匹配的文本。它们可以使用< 《a href="https://regexper.cn/brackets.html" translate="no">捕获组从步骤文本中提取数据并将其传递给步骤代码。
Python-specific
在Python脚本中,您可以直接传递正则表达式对象。也就是说,Python的re.compile函数返回的对象。如果您不想导入re模块,您也可以传递一个字符串以及可选参数regexp设置为True,如下所示
@Step(r"user starts the (\w+) application", regexp=True) def step(context, appName): ...
我们也建议您使用Python的原生字符串语法(r"..." resp. r'...')来指定字符串,以避免在正则表达式中转义反斜杠字符。
Tcl-specific
在Tcl脚本中,没有正则表达式值。相反,使用以下片段中的-rx选项调用Step命令
Step -rx "This is a reg.lar expression$" {context} { ... }
正则表达式示例
例如,上述示例可以用正则表达式表示如下
@Step(r"user starts the (\w+) application", regexp=True) def step(context, appName): startApplication(appName)
Step(/user starts the (\w+) application/, function(context, appName) { startApplication(appName); });
Step -rx {user starts the (\w+) application} {context appName} { startApplication $appName }
Step(/user starts the (\w+) application/) do |context, appName| Squish::startApplication(appName) end
use Squish::BDD; Step qr/user starts the (\w+) application/, sub { my %context = %{shift()}; my $appName = shift(); startApplication($appName); };
BDD上下文对象
每个BDD实现函数都传递给第一个参数的context单例对象。场景钩子、步骤钩子和步骤实现的上下文对象提供了不同的属性。(有关钩子详细信息,请参阅钩子API。)
场景钩子的上下文对象(@OnScenarioStart, @OnScenarioEnd)
| 属性 | 说明 |
|---|---|
| context.title | 当前情景的标题(名称) |
| context.userData | 可以用于在步骤和钩子之间传递任意数据的额外属性(通常作为列表、映射或字典)。(默认:None、null等。) |
步骤钩子的上下文对象(@OnStepStart, @OnStepEnd)
| 属性 | 说明 |
|---|---|
| context.text | 当前步骤的文本(名称) |
| context.userData | 可以用于在步骤和钩子之间传递任意数据的额外属性(通常作为列表、映射或字典)。(默认:None、null等。) |
步骤实现的上下文对象
| 属性 | 说明 |
|---|---|
| context.multiLineText | 如果它被传递到步骤中,则存储多行文本。 |
| context.table | 如果它被传递到步骤中,则存储表格数据。 |
| context.userData | 可以用于在步骤和钩子之间传递任意数据的额外属性(通常作为列表、映射或字典)。(默认:None、null等。) |
有关使用userData、table和multiLineText的示例在以下章节中。
使用userData在步骤之间传递数据
传递给所有步骤(和钩子)的context参数具有一个可以用于在步骤之间传递数据的userData字段。此属性最初为空,但是可以在步骤内部对其进行写入。例如,考虑以下功能文件
Feature: Shopping Cart
Scenario: Adding three items to the shopping cart
Given I have an empty shopping cart
When I add bananas to the shopping cart
And I add apples to the shopping cart
And I add milk to the shopping cart
Then I have 3 items in the shopping cart该场景的隐式状态(当前购物车的最新内容)可以使用userData进行建模
@Given("I have an empty shopping cart") def step(context): context.userData = [] @When("I add |word| to the shopping cart") def step(context, itemName): context.userData.append(itemName) @Then("I have |integer| items in the shopping cart") def step(context, expectedAmount): test.compare(len(context.userData), expectedAmount)
Given("I have an empty shopping cart", function(context) { context.userData = []; }); When("I add |word| to the shopping cart", function(context, itemName) { context.userData.push(itemName); }); Then("I have |integer| items in the shopping cart", function(context, expectedAmount) { test.compare(context.userData.length, expectedAmount); });
Given("I have an empty shopping cart", sub { my $context = shift(); $context->{userData} = []; }); When("I add |word| to the shopping cart", sub { my($context, $itemName) = @_; push @{$context->{userData}}, $itemName; }); Then("I have |integer| items in the shopping cart", sub { my($context, $expectedAmount) = @_; test::compare(scalar(@{$context->{userData}}), $expectedAmount); });
Given "I have an empty shopping cart" {context} { $context userData {} } When "I add |word| to the shopping cart" {context itemName} { set items [$context userData] lappend items $itemName $context userData $items } Then "I have |integer| items in the shopping cart" {context expectedAmount} { set items [$context userData] test compare [llength $items] $expectedAmount }
Given("I have an empty shopping cart") do |context| context.userData = [] end When("I add |word| to the shopping cart") do |context, itemName| context.userData.push itemName end Then("I have |integer| items in the shopping cart") do |context, expectedAmount| Test.compare context.userData.count, expectedAmount end
这里,userData字段跟踪添加到购物车中的项目,因为这些是与测试上下文相关的额外数据。它也可以用于在多AUT测试用例中跟踪活动的应用程序上下文对象等。
注意:userData永远不会被清除,除非您使用例如OnScenarioStart钩子显式地清除它。
访问表格和多行文本
context 对象向步骤公开了可选的额外参数,包括具有 multiLineText 和 table 属性的。
multiLineText 属性返回步骤的任意多行文本参数。文本作为字符串列表返回,每个字符串代表一行。例如,在以下步骤中
Given I create a file containing """ [General] Mode=Advanced """
可以通过以下方式访问多行文本
@Step("I create a file containing") def step(context): text = context.multiLineText f = open("somefile.txt", "w") f.write("\n".join(text)) f.close()
Step("I create a file containing", function(context) { var text = context.multiLineText; var file = File.open("somefile.txt", "w"); file.write(text.join("\n")); file.close(); });
Step {I create a file containing} {context} {
set text [$context multiLineText]
set f [open "somefile.txt" "w"]
puts $f [join $text "\n"]
close $f
}Step("I create a file containing") do |context| File.open("somefile.txt", 'w') { |file| file.write(context.multiLineText.join("\n")) } end
use Squish::BDD; Step "I create a file containing", sub { my %context = %{shift()}; my @multiLineText = @{$context{'multiLineText'}}; my $text = join("\n", @multiLineText); open(my $fh, '>', 'somefile.txt') or die; print $fh $text; close $fh; };
table 属性返回步骤的任意文本参数。表格作为列表的列表返回:每个内部列表表示一行,列表的各个元素表示单元格。例如,在以下步骤中
Given I enter the records | firstName | lastName | age | | Bob | Smith | 42 | | Alice | Thomson | 27 | | John | Martin | 33 |
可以通过以下方式访问表格参数
@Step("I enter the records") def step(context): table = context.table # Drop initial row with column headers for row in table[1:]: first = row[0] last = row[1] age = row[2] # ... handle first/last/age
Step("I enter the records", function(context) { var table = context.table; // Skip initial row with column headers by starting at index 1 for (var i = 1; i < table.length; ++i) { var first = table[i][0]; var last = table[i][1]; var age = table[i][2]; // ... handle first/last/age } });
Step {I enter the records} {context} {
set table [$context table]
# Drop initial row with column headers
foreach row [lreplace $table 0 0] {
foreach {first last age} $row break
# ... handle $first/$last/$age
}
}Step("I enter the records") do |context| table = context.table # Drop initial row with column headers table.shift for first,last,age in table do # ... handle first/last/age end end
use Squish::BDD; Step "I enter the records", sub { my %context = %{shift()}; my @table = @{$context{'table'}}; # Drop initial row with column headers shift(@table); for my $row (@table) { my ($first, $last, $age) = @{$row}; # ... handle $first/$last/$age } };
使用 Given/When/Then 定义步骤实现
除了 Step 之外,Squish 还支持使用三个更多函数来定义步骤实现,这三种函数称为 Given、When 和 Then。这三个函数遵循与 Step 相同的语法,但在执行步骤时产生略微不同的行为,因为使用 Step 注册的步骤实施与使用其他三个函数之一注册的步骤实施匹配的步骤文本要多。以下表格说明了这种差异
| 步骤文本 | Step("...", ..) 匹配 | Given("...", ..) 匹配 | When("...", ..) 匹配 | Then("...", ..) 匹配 |
|---|---|---|---|---|
| Given 我说你好 | 是 | 是 | 否 | 否 |
| When 我说你好 | 是 | 否 | 是 | 否 |
| Then 我说你好 | 是 | 否 | 否 | 是 |
| And 我说你好 | 是 | 也许 | 也许 | 也许 |
| 但是我说你好 | 是 | 也许 | 也许 | 也许 |
通过 Step 注册的图案始终匹配,无论当前的步骤以什么关键词开始。 Given、When 和 Then 只有当步骤以匹配的关键词或同义词(如 And 或 But)开头时才会匹配。
注意: 通常建议使用 Given/When/Then 而不是 Step 来注册模式,因为这也可以提高 squishide 提供的自动完成功能的效率。
通过 Given/When/Then 注册的模式是否匹配以 And 或 But 开头的步骤取决于 And/But 前的关键词。考虑以下
Feature: Some Feature
Scenario: Some Scenario
Given I am hungry
And I'm in the mood for 20000 of something
But I do not like peas
Then I will enjoy rice.
在此功能描述中,以 "And" 和 "But" 开头的行紧接在以 "Given" 开头的行之后,它们是同义的。这意味着只有通过 Step 或 Given 注册的模式会匹配步骤 "And I'm in the mood for 20000 of something"。
步骤查找顺序 & 覆盖共享步骤实现
在执行 BDD 测试时,Squish 将在决定哪个步骤实现匹配给定的步骤文本时遵循一个特定的顺序。步骤实现是基于启动 BDD 测试时加载包含步骤实现的源文件的顺序来考虑的。默认情况下,此顺序如下
- 当前测试用例的
steps子目录。 - 当前测试套件的
shared/scripts/steps目录。
此顺序使得本地测试用例中定义的步骤比测试套件的共享脚本目录中定义的步骤优先。
实际上,Squish不仅仅会加载上述目录中存储的步骤实现,还会加载子目录中的步骤实现。例如,您可以在像steps/basic/basicsteps.py或steps/uisteps/mainwindowsteps.js等文件中注册步骤实现。
注意:从这些目录加载步骤实现列表不是硬编码到Squish中。相反,它是一个在测试用例目录中存储的test.*文件中指定的目录列表(例如,JavaScript测试的test.js。您可以调整单个测试的顺序,或者通过编辑文件scriptmodules/*/bdt_driver_template.*(例如,scriptmodules/javascript/bdt_driver_template.js)来为所有新创建的测试调整顺序。
尽管注册具有相同模式的两个步骤实现会导致错误,但注册在两个不同目录中定义的两个步骤实现不会触发错误。这意味着您可以通过在特定测试用例文件中使用相同的模式来“覆盖”共享步骤。
从步骤实现中影响场景执行
可以定义步骤实现,使得当前场景被中止,并跳过所有后续步骤(如果有的话)。这对于后续步骤依赖于某些条件(例如某些文件存在)但前置步骤未能建立/验证这些条件失败的情况非常有用。
为了在当前场景中跳过后续步骤,步骤实现可以返回一个特殊值 - 具体的名称取决于相应的脚本语言。
@Step("I create a file containing") def step(context): try: text = context.multiLineText f = open("somefile.txt", "w") f.write("\n".join(text)) f.close() except: # Failed to create file; skip subsequent steps in current scenario return AbortScenario
Step("I create a file containing", function(context) { try { var text = context.multiLineText; var file = File.open("somefile.txt", "w"); file.write(text); file.close(); } catch (e) { // Failed to create file; skip subsequent steps in current scenario return AbortScenario; } });
Step {I create a file containing} {context} {
if {[catch {
set text [$context multiLineText]
set f [open "somefile.txt" "w"]
puts $f [join $text "\n"]
close $f
}]} then {
# Failed to create file; skip subsequent steps in current scenario
return AbortScenario
}
}Step("I create a file containing") do |context| begin File.open("somefile.txt", 'w') { |file| file.write(context.multiLineText.join("\n")) } rescue Exception => e # Failed to create file; skip subsequent steps in current scenario next ABORT_SCENARIO end end
use Squish::BDD; Step "I create a file containing", sub { my %context = %{shift()}; my @multiLineText = @{$context{'multiLineText'}}; my $text = join("\n", @multiLineText); if (open(my $fh, '>', 'somefile.txt')) { print $fh $text; close $fh; } else { # Failed to create file; skip subsequent steps in current scenario return ABORT_SCENARIO; } };
通过钩子在测试执行期间执行操作
在一些情况下,某些操作在某些事件发生之前或之后需要执行。这种需求出现于以下情况:
- 在测试执行开始之前应该初始化全局变量。
- 在执行功能之前需要启动应用程序。
- 在场景执行之后应删除临时文件。
Squish允许定义函数,这些函数被连接到测试执行序列。函数可以与某些事件相关联,并在事件发生之前或之后执行。您可以为事件注册任意多的函数:钩子函数将以它们定义的顺序被调用。函数可以与以下任何事件相关联:
- OnFeatureStart/
OnFeatureEnd:这些事件在特定Feature的第一个/最后一个场景执行之前/之后触发。传递给与这些事件之一相关的函数的context参数提供了title字段,该字段提供了即将执行( resp. 刚刚完成执行)的功能的标题。 - OnScenarioStart/
OnScenarioEnd:这些事件在特定Scenario的第一个/最后一个步骤执行之前/之后触发。《OnScenarioStart》事件也会在执行任何Background之前触发。传递给与这些事件之一相关的函数的context参数提供了title字段,该字段提供了即将执行( resp. 刚刚完成执行)的场景的标题。 - OnStepStart/
OnStepEnd:这些事件在每个特定Step中的代码执行之前/之后触发。传递给与这些事件之一相关的函数的context参数提供了text字段,该字段提供了即将执行( resp. 刚刚完成执行)的步骤的文本。
您可以使用特定语言的API将代码与以下任一事件关联,当这些事件发生时执行代码。通常,注册事件的函数名称与事件名称相同,除了Python,因为Python使用装饰器,所以函数名称不重要。这些函数与您在脚本中注册步骤实现使用的Step函数相同的脚本文件中注册。
以下是一些示例
设置一个OnFeatureStart钩子来设置全局变量
OnFeatureStart(function(context) {
counter = 0;
inputFileName = "sample.txt";
});my $inputFileName;
my $counter;
OnFeatureStart sub {
$counter = 0;
$inputFileName = 'sample.txt';
};@OnFeatureStart
def hook(context):
global counter
global inputFileName
counter = 0
inputFileName = "sample.txt"OnFeatureStart do @counter = 0 @inputFileName = 'sample.txt' end
OnFeatureStart {context} {
global counter
global inputFileName
set $counter 0
set $inputFileName "sample.txt"
}注册OnScenarioStart和OnScenarioEnd事件处理程序以启动和停止AUT
OnScenarioStart(function(context) {
startApplication("addressbook");
});
OnScenarioEnd(function(context) {
currentApplicationContext().detach();
});OnScenarioStart sub {
::startApplication('addressbook');
};
OnScenarioEnd sub {
::currentApplicationContext.detach();
};@OnScenarioStart
def hook(context):
startApplication("addressbook")
@OnScenarioEnd
def hook(context):
currentApplicationContext().detach()OnScenarioStart do |context| Squish::startApplication 'addressbook' end OnScenarioEnd do |context| Squish::currentApplicationContext.detach end
OnScenarioStart {context} {
startApplication "addressbook"
}
OnScenarioEnd {context} {
applicationContext [currentApplicationContext] detach
}每当即将执行提到单词delete的步骤时,生成额外的警告日志输出
OnStepStart(function(context) {
var text = context["text"];
if (text.search("delete") > -1) {
test.warning("About to execute dangerous step: " + text);
}
});OnStepStart sub {
my %context = %{shift()};
if (index( $context{text}, 'delete') != -1) {
::test::warning("About to execute dangerous step: $context{text}");
}
};@OnStepStart
def hook(context):
text = context.text
if text.find("delete") > -1:
test.warning("About to execute dangerous step: %s" % text)OnStepStart do |context| if context['text'].include? 'delete' Squish::Test.warning "About to execute dangerous step: #{context['text']}" end end
OnStepStart {context} {
set text [$context text]
if {[string first "delete" $text] > -1} {
test warning "About to execute dangerous step: $text"
}
}BDD测试用例的结构
所有BDD测试用例都从在所选脚本语言中运行一个常规脚本开始。此脚本由系统自动生成,可以找到与相应的test.feature文件相同的目录中,并称为test.xy,其中xy = (pl|py|rb|js|tcl)。它看起来像这样
source(findFile('scripts', 'python/bdd.py'))
setupHooks('../shared/scripts/bdd_hooks.py')
collectStepDefinitions('./steps', '../shared/steps')
def main():
testSettings.throwOnFailure = True
runFeatureFile('test.feature')source(findFile('scripts', 'javascript/bdd.js'));
setupHooks(['../shared/scripts/bdd_hooks.js']);
collectStepDefinitions(['./steps', '../shared/steps']);
function main()
{
testSettings.throwOnFailure = true;
runFeatureFile("test.feature");
}use warnings;
use strict;
use Squish::BDD;
setupHooks("../shared/scripts/bdd_hooks.pl");
collectStepDefinitions("./steps", "../shared/steps");
sub main {
testSettings->throwOnFailure(1);
runFeatureFile("test.feature");
}# encoding: UTF-8
require 'squish'
require 'squish/bdd'
include Squish::BDD
setupHooks "../shared/scripts/bdd_hooks.rb"
collectStepDefinitions "./steps", "../shared/steps"
def main
Squish::TestSettings.throwOnFailure = true
Squish::runFeatureFile "test.feature"
endsource [findFile "scripts" "tcl/bdd.tcl"]
Squish::BDD::setupHooks "../shared/scripts/bdd_hooks.tcl"
Squish::BDD::collectStepDefinitions "./steps" "../shared/steps"
proc main {} {
testSettings set throwOnFailure true
runFeatureFile "test.feature"
}此脚本中调用的某些函数是Squish BDD API的一部分,但在常规测试用例中很少使用。本节将解释这些函数的作用,以防您想在基于脚本的测试中重用BDD步骤或其他BDD功能。
设置钩子
首先,在每次BDD测试用例的开始调用setupHooks(),以扫描和设置在通过钩子执行测试执行中的操作中描述的钩子函数。
收集步骤定义
接下来,使用collectStepDefinitions()来扫描和导入作为函数参数指定的目录中找到的步骤定义。可以提供一个或多个参数,并且越早提供的参数优先级越高。步骤实现通常位于名为steps.xy的文件中,其中xy = (pl|py|rb|js|tcl)。
collectStepDefinitions('./steps', '../shared/steps')
collectStepDefinitions('./steps', '../shared/steps');
use Squish::BDD; collectStepDefinitions("./steps", "../shared/steps");
include Squish::BDD collectStepDefinitions "./steps", "../shared/steps"
source [findFile "scripts" "tcl/bdd.tcl"] Squish::BDD::collectStepDefinitions "./steps" "../shared/steps"
因此,在上面的示例中,如果也在Test Suite Resources中找到同名步骤定义,则将优先使用当前Test Case Resources中的步骤定义。
最后,在集成了钩子和收集了步骤后,可以在BDD .feature文件上调用runFeatureFile(),以运行指定Gherkin功能文件中的所有场景。
使用From关键字从外部文件读取表
有时,将场景概述占位符的表数据或上下文表存储在外部文件中可能比直接在功能文件中存储表更加实际。例如,这些是大型数据集或在多个BDD测试用例中使用相同数据集的情况。
要从外部文件读取表,我们可以使用From关键字后跟相对.feature文件的文件路径。例如
Scenario: Register orders
Given orderbook is running
And orderbook is empty
When I register orders
From testdata/order_list.txt
Then orderbook is not empty在这种情况下,我们将order_list.txt文件存储在我们的功能文件旁边的testdata目录中。它包含熟悉的Gherkin格式的表。
# testdata/order_list.txt | name | date | type | amount | | Amy | 04.02.2017 | apple | 2 | | Josh | 04.02.2017 | peach | 3 | | Marc | 05.02.2017 | cheese | 14 | | Lea | 07.02.2017 | soda | 1 |
Squish接受以下格式的表文件
- 原始文本文件(.txt或无扩展名)
- 逗号分隔值(.csv)
- 制表符分隔值(.tsv)
- Microsoft Excel工作表(.xls)
当从原始文本文件(.txt或无扩展名)读取表时,Squish就像在功能文件中内联一样进入文件并读取(因此我们也可以在文本表文件内使用进一步的From语句)。对于所有其他格式,Squish直接解析记录。
From 默认情况下使用表的表头(第一行)来将表列映射到脚本变量或场景大纲占位符,但在读取表文件之前可以定义一个自定义表头来使用。
| customer | date | item | count | From testdata/order_list.txt
这使得与我们的测试脚本一起使用表文件变得更加容易。
注意:标题的列数仍然必须与表的列数匹配。
©2024 The Qt Company Ltd. 本文件中包含的文档贡献者是各自所有者的版权。
此处提供的文档是根据自由软件基金会发布并由其发布的《GNU自由文档许可》第1.3版条款许可的。
Qt及其相关标志是芬兰及其它全球国家的Qt公司的商标。所有其他商标为其各自所有者的财产。