JavaFX Squish BDD 指南
教程:设计行为驱动开发(BDD)测试
本教程将向您展示如何为示例应用程序创建、运行和修改行为驱动开发(BDD)测试。您将了解Squish最常用的功能。完成本教程后,您将能够为自己的应用程序编写测试。
注意:如果需要视频指导,可以在的Qt Academy找到关于Squish行为驱动测试的40分钟在线课程。
在本章中,我们将使用用JavaFX编写的简单地址簿应用程序作为我们测试的应用程序(AUT)。这是一个非常基本的应用程序,允许用户加载现有的地址簿或创建一个新的地址簿,添加、编辑和删除条目。截图显示了用户正在添加一个新名称和地址的应用程序。
行为驱动开发(BDD)简介
行为驱动开发(BDD)是测试驱动开发(TDD)的扩展,它将验收标准定义放在开发过程的开端,而不是在软件开发完成后编写测试。测试后的代码更改可能有循环。
行为驱动测试由一系列功能
文件组成,这些文件通过一个或多个场景
描述产品的功能,这些场景定义了预期的应用程序行为。每个场景
都是一系列步骤的集合,这些步骤代表需要为该场景
测试的动作或验证。
BDD侧重于预期的应用程序行为,而不是实现细节。因此,BDD测试使用人类可读的领域特定语言(DSL)来描述。由于这种语言不是技术的,所以普通人也可以创建这样的测试,包括产品所有者、测试人员或业务分析师。此外,在产品开发过程中,此类测试作为活的文档。在Squish的使用中,应使用Gherkin语法来创建BDD测试。之前编写的(产品)规范(BDD测试)可以转换为可执行的测试。此逐步教程介绍使用squishide
支持来自动化BDD测试。
Gherkin语法
在Gherkin文件中,通过一个或多个场景描述了应用程序的预期行为,以描述产品的功能。以下是对地址簿示例应用程序“地址簿填充”特性的示例。
Feature: Filling of addressbook As a user I want to fill the addressbook with entries Scenario: Initial state of created address book Given addressbook application is running When I create a new addressbook Then addressbook should have zero entries Scenario: State after adding one entry Given addressbook application is running When I create a new addressbook And I add a new person 'John','Doe','[email protected]','500600700' to address book Then '1' entries should be present Scenario: State after adding two entries Given addressbook application is running When I create a new addressbook And I add new persons to address book | forename | surname | email | phone | | John | Smith | john@m.com | 123123 | | Alice | Thomson | alice@m.com | 234234 | Then '2' entries should be present Scenario: Forename and surname is added to table Given addressbook application is running When I create a new addressbook When I add a new person 'Bob','Doe','[email protected]','123321231' to address book Then previously entered forename and surname shall be at the top
以上大部分是自由文本(不一定要是英文)。固定的是功能
/场景
结构和“给定”、“和”、“当”和“然后”等起始关键字。这些关键字中的每一个都标记了一个定义预条件、用户动作和预期结果的步骤。上述应用程序行为描述可以传递给软件开发人员以实现这些功能,同时还可以将同一描述传递给软件测试人员以实现自动化测试。
测试实施
创建测试套件
首先,我们需要创建一个测试套件,它是一个所有测试用例的容器。启动squishide并选择文件 > 新建测试套件。按照新建测试套件向导,提供测试套件名称,选择您选择的Java工具包和脚本语言,最后将地址簿应用程序注册为AUT。有关创建新测试套件的更多详细信息,请参阅创建测试套件。
创建测试用例
Squish提供两种类型的测试用例:“脚本测试用例”和“BDD测试用例”。由于“脚本测试用例”是默认的,为了创建新的“BDD测试用例”,我们需要通过点击旁边展开按钮来使用上下文菜单(旁边的新建脚本测试用例 )并选择新建BDD测试用例。 squishide
会记住您的选择,并将在将来点击按钮时将“BDD测试用例”设置为默认。
新创建的BDD测试用例包括一个test.feature
文件(在创建新的BDD测试用例时填入Gherkin模板),一个名为test.(py|js|pl|rb|tcl)
的文件,该文件将驱动执行(无需编辑此文件),以及一个名为steps/steps.(py|js|pl|rb|tcl)
的测试套件资源文件,其中将放置步骤实现代码。
我们需要将Gherkin模板替换为地址簿示例应用程序的Feature
。为此,复制下面的Feature
描述并将其粘贴到Feature
文件中。
Feature: Filling of addressbook
As a user I want to fill the addressbook with entries
Scenario: Initial state of created address book
Given addressbook application is running
When I create a new addressbook
Then addressbook should have zero entries
在编辑test.feature
文件时,对于每个未定义的步骤,都会显示Feature
文件警告“未找到 implementation”。实现位于steps
子目录中,在测试用例资源或测试套件资源中。现在运行我们的Feature
测试将会使测试在第一个步骤失败,因为找不到匹配的步骤定义,后续的步骤将被跳过。
记录步骤实现
为了记录Scenario
,点击位于Test Case Resources视图的Scenarios选项卡中相应Scenario
旁边的Record( )。
这将导致Squish运行AUT,以便我们可以与之交互。此外,会显示控制栏,其中列出了所有需要记录的步骤。现在所有与AUT的交互或添加到脚本中的任何验证点都将记录在第一个步骤Given addressbook application is running
下(该步骤在控制栏的步骤列表中被加粗)。为了验证这个先决条件是否满足,我们将添加一个验证点。要做到这一点,点击控制栏上的Verify并选择Properties。
结果,squishide
进入Spy模式,显示所有应用程序对象及其属性。在选择Properties View中,选中“showing”属性的复选框。最后,点击Save and Insert Verifications按钮。 squishide
消失,控制栏再次显示。
当我们完成每个步骤后,我们可以通过点击控制栏左侧当前步骤旁边的Finish Recording Step( )箭头按钮移动到下一个未定义的步骤(播放之前定义的步骤)。
接下来,对于步骤When I create a new addressbook
,在AddressBook应用程序的工具栏上点击New按钮。再次,点击Finish Recording Step( )以进入下一个步骤。
最后,对于步骤 Then addressbook should have zero entries
,验证包含地址条目的表是否为空。为了记录这个验证,点击Squish控制栏上的Verify,选择Properties。在Application Objects视图中,导航或使用对象选择器()来选择(不检查)包含地址簿条目的TableView(在我们的例子中,这个表为空)。
展开Properties视图中的项目树节点,您将看到那个下的布尔型空属性。检查它,然后按Save and Insert Verifications。最后,点击最后一个Finish Recording Step()箭头按钮来结束记录。Squish生成以下步骤定义
@Given("addressbook application is running") def step(context): startApplication("AddressBook.jar") stage = waitForObject(names.address_Book_Stage) test.compare(stage.showing, True) win = ToplevelWindow.byObject(stage) win.setForeground() @When("I create a new addressbook") def step(context): mouseClick(waitForObject(names.fileNewButton_button)) @Then("addressbook should have zero entries") def step(context): test.compare(waitForObjectExists(names.address_Book_Unnamed_itemTbl_table_view).items.empty, True)
Given("addressbook application is running", function(context) { startApplication("AddressBook.jar"); var stage = waitForObject(names.addressBookStage); test.compare(stage.showing, true); var win = ToplevelWindow.byObject(stage); win.setForeground(); }); When("I create a new addressbook", function(context) { mouseClick(waitForObject(names.fileNewButtonButton)); }); Then("addressbook should have zero entries", function(context) { test.compare(waitForObjectExists(names.addressBookUnnamedItemTblTableView).items.empty, true); });
Given("addressbook application is running", sub { my $context = shift; startApplication("AddressBook.jar"); my $stage = waitForObjectExists($Names::address_book_stage); test::compare($stage->showing, 1); my $win = Squish::ToplevelWindow->byObject($stage); $win->setForeground; }); When("I create a new addressbook", sub { my $context = shift; mouseClick(waitForObject($Names::filenewbutton_button)); }); Then("addressbook should have zero entries", sub { my $context = shift; test::compare(waitForObjectExists($Names::address_book_unnamed_itemtbl_table_view)->items->empty, 1); });
Given("addressbook application is running") do |context| startApplication("AddressBook.jar") stage = waitForObject(Names::Address_Book_Stage) Test.compare(stage.showing, true) win = ToplevelWindow::byObject(stage) win.setForeground() end When("I create a new addressbook") do |context| mouseClick(waitForObject(Names::FileNewButton_button)) end Then("addressbook should have zero entries") do |context| Test.compare(waitForObjectExists(Names::Address_Book_Unnamed_itemTbl_table_view).items.empty, true) end
Given "addressbook application is running" {context} { startApplication "AddressBook.jar" set stage [waitForObject $names::Address_Book_Stage] test compare [property get $stage showing] true set win [Squish::ToplevelWindow byObject $stage] $win setForeground } When "I create a new addressbook" {context} { invoke mouseClick [waitForObject $names::fileNewButton_button] } Then "addressbook should have zero entries" {context} { test compare [property get [property get [waitForObjectExists $names::Address_Book_Unnamed_itemTbl_table_view] items] empty] true }
由于记录了startApplication()
调用,应用程序在第一步的开始时自动启动。在每个场景的末尾,会调用OnScenarioEnd
钩子,这将导致在应用程序上下文中调用detach()
。因为应用程序使用startApplication()
启动,这导致其终止。此钩子函数位于Scripts标签中Test Suite Resources视图的文件bdd_hooks.(py|js|pl|rb|tcl)
中。您可以在其中定义额外的钩子函数。有关所有可用钩子的列表,请参阅通过钩子执行测试执行中的操作。
@OnScenarioEnd def hook(context): for ctx in applicationContextList(): ctx.detach()
OnScenarioEnd(function(context) { applicationContextList().forEach(function(ctx) { ctx.detach(); }); });
OnScenarioEnd(sub { foreach (applicationContextList()) { $_->detach(); } });
OnScenarioEnd do |context| applicationContextList().each { |ctx| ctx.detach() } end
OnScenarioEnd { context } { foreach ctx [applicationContextList] { applicationContext $ctx detach } }
步骤参数化
到目前为止,我们的步骤没有使用任何参数,所有值都是硬编码的。Squish具有不同类型的参数,如any
、integer
或word
,允许我们的步骤定义更具可重用性。让我们在Feature
文件中添加一个新的Scenario
,这将为测试数据和期望结果提供步骤参数。将以下部分复制到您的Feature文件中。
Scenario: State after adding one entry Given addressbook application is running When I create a new addressbook And I add a new person 'John','Doe','[email protected]','500600700' to address book Then '1' entries should be present
保存Feature
文件后,squishide
提供了提示,表示只需实现2个步骤:When I add a new person 'John', 'Doe','[email protected]','500600700' to address book
和Then '1' entries should be present
。其余步骤已经有了匹配的步骤实现。
要记录缺失的步骤,在Test Suites视图中的测试案例名称旁边点击Record()。脚本将播放直到遇到缺失的步骤,然后提示您实现它。如果您选择Add按钮,则可以输入新条目的信息。点击Finish Recording Step()按钮转到下一个步骤。对于第二个缺失的步骤,我们可以记录一个类似于Then addressbook should have zero entries
的对象属性验证,但针对items.length
。
现在,我们通过将值替换为参数类型来参数化生成的步骤实现。由于我们希望能够添加不同的名称,将'John'替换为'|word|'。注意,每个参数将按步骤描述名的出现顺序传递给步骤实现函数。通过将输入值编辑为关键字来完成参数化,如下所示示例步骤When I add a new person 'John', 'Doe','[email protected]','500600700'
to address book
@When("I add a new person '|word|','|word|','|any|','|integer|' to address book") def step(context, forename, lastname, email, phone): mouseClick(waitForObject(names.editAddButton_button)) type(waitForObject(names.address_Book_Add_forenameText_text_input_text_field), forename) type(waitForObject(names.address_Book_Add_surnameText_text_input_text_field), lastname) type(waitForObject(names.address_Book_Add_emailText_text_input_text_field), email) type(waitForObject(names.address_Book_Add_phoneText_text_input_text_field), phone) mouseClick(waitForObject(names.address_Book_Add_OK_button))
When("I add a new person '|word|','|word|','|any|','|integer|' to address book", function(context, forename, lastname, email, phone) { mouseClick(waitForObject(names.editAddButtonButton)); type(waitForObject(names.addressBookAddForenameTextTextInputTextField), forename); type(waitForObject(names.addressBookAddSurnameTextTextInputTextField), lastname); type(waitForObject(names.addressBookAddEmailTextTextInputTextField), email); type(waitForObject(names.addressBookAddPhoneTextTextInputTextField), phone); mouseClick(waitForObject(names.addressBookAddOKButton));
When("I add a new person '|word|','|word|','|any|','|integer|' to address book", sub { my ($context, $forename, $surname, $email, $phone) = @_; mouseClick(waitForObject($Names::editaddbutton_button)); type(waitForObject($Names::address_book_add_forenametext_text_input_text_field), $forename); type(waitForObject($Names::address_book_add_surnametext_text_input_text_field), $surname); type(waitForObject($Names::address_book_add_emailtext_text_input_text_field), $email); type(waitForObject($Names::address_book_add_phonetext_text_input_text_field), $phone); mouseClick(waitForObject($Names::address_book_add_ok_button));
When("I add a new person '|word|','|word|','|any|','|integer|' to address book") do |context, forename, surname, email, phone| mouseClick(waitForObject(Names::EditAddButton_button)) type(waitForObject(Names::Address_Book_Add_forenameText_text_input_text_field), forename) type(waitForObject(Names::Address_Book_Add_surnameText_text_input_text_field), surname) type(waitForObject(Names::Address_Book_Add_emailText_text_input_text_field), email) type(waitForObject(Names::Address_Book_Add_phoneText_text_input_text_field), phone) mouseClick(waitForObject(Names::Address_Book_Add_OK_button))
When "I add a new person '|word|','|word|','|any|','|integer|' to address book" {context forename surname email phone} { invoke mouseClick [waitForObject $names::editAddButton_button] invoke type [waitForObject $names::Address_Book_Add_forenameText_text_input_text_field] $forename invoke type [waitForObject $names::Address_Book_Add_surnameText_text_input_text_field] $surname invoke type [waitForObject $names::Address_Book_Add_emailText_text_input_text_field] $email invoke type [waitForObject $names::Address_Book_Add_phoneText_text_input_text_field] $phone invoke mouseClick [waitForObject $names::Address_Book_Add_OK_button]
如果我们把最终的Then
作为缺失步骤进行记录,并且在表格中检查items.length是否为1,我们可以修改这个步骤,让它接受参数,以便将来验证其他整数值。
@Then("'|integer|' entries should be present") def step(context, count): test.compare(waitForObjectExists(names.address_Book_Unnamed_itemTbl_table_view).items.length, count)
Then("'|integer|' entries should be present", function(context, count) { test.compare(waitForObjectExists(names.addressBookUnnamedItemTblTableView).items.length, count);
Then("'|integer|' entries should be present", sub { my ($context, $count) = @_; test::compare(waitForObjectExists($Names::address_book_unnamed_itemtbl_table_view)->items->length, $count);
Then("'|integer|' entries should be present") do |context, count| Test.compare(waitForObjectExists(Names::Address_Book_Unnamed_itemTbl_table_view).items.length, count)
Then "'|integer|' entries should be present" {context count} { test compare [property get [property get [waitForObjectExists $names::Address_Book_Unnamed_itemTbl_table_view] items] length] $count
为表格中的步骤提供参数
下一个Scenario
将测试向通讯录添加多个条目。我们可以仅通过使用不同数据重复使用步骤When I add a new person John','Doe','[email protected]','500600700' to address book
。但让我们定义一个新的步骤称为When I add a new person to address book
,它可以处理表格中的数据。
Scenario: State after adding two entries Given addressbook application is running When I create a new addressbook And I add new persons to address book | forename | surname | email | phone | | John | Smith | john@m.com | 123123 | | Alice | Thomson | alice@m.com | 234234 | Then '2' entries should be present
处理此类表格的步骤实现如下
@When("I add new persons to address book") def step(context): table = context.table # Drop initial row with column headers table.pop(0) for (forename, surname, email, phone) in table: mouseClick(waitForObject(names.editAddButton_button)) type(waitForObject(names.address_Book_Add_forenameText_text_input_text_field), forename) type(waitForObject(names.address_Book_Add_surnameText_text_input_text_field), surname) type(waitForObject(names.address_Book_Add_emailText_text_input_text_field), email) type(waitForObject(names.address_Book_Add_phoneText_text_input_text_field), phone) mouseClick(waitForObject(names.address_Book_Add_OK_button))
When("I add new persons to address book", function(context) { var table = context.table; for (var i = 1; i < table.length; ++i) { var row = table[i]; mouseClick(waitForObject(names.editAddButtonButton)); type(waitForObject(names.addressBookAddForenameTextTextInputTextField), row[0]); type(waitForObject(names.addressBookAddSurnameTextTextInputTextField), row[1]); type(waitForObject(names.addressBookAddEmailTextTextInputTextField), row[2]); type(waitForObject(names.addressBookAddPhoneTextTextInputTextField), row[3]); mouseClick(waitForObject(names.addressBookAddOKButton)); } });
When("I add new persons to address book", sub { my $context = shift; my $table = $context->{'table'}; # Drop initial row with column headers shift(@{$table}); for my $row (@{$table}) { my ($forename, $surname, $email, $phone) = @{$row}; mouseClick(waitForObject($Names::editaddbutton_button)); type(waitForObject($Names::address_book_add_forenametext_text_input_text_field), $forename); type(waitForObject($Names::address_book_add_surnametext_text_input_text_field), $surname); type(waitForObject($Names::address_book_add_emailtext_text_input_text_field), $email); type(waitForObject($Names::address_book_add_phonetext_text_input_text_field), $phone); mouseClick(waitForObject($Names::address_book_add_ok_button)); } });
When("I add new persons to address book") do |context| table = context.table # Drop initial row with column headers table.shift for forename, surname, email, phone in table do mouseClick(waitForObject(Names::EditAddButton_button)) type(waitForObject(Names::Address_Book_Add_forenameText_text_input_text_field), forename) type(waitForObject(Names::Address_Book_Add_surnameText_text_input_text_field), surname) type(waitForObject(Names::Address_Book_Add_emailText_text_input_text_field), email) type(waitForObject(Names::Address_Book_Add_phoneText_text_input_text_field), phone) mouseClick(waitForObject(Names::Address_Book_Add_OK_button)) end end
When "I add new persons to address book" {context} { set table [$context table] # Drop initial row with column headers foreach row [lreplace $table 0 0] { foreach {forename surname email phone} $row break invoke mouseClick [waitForObject $names::editAddButton_button] invoke type [waitForObject $names::Address_Book_Add_forenameText_text_input_text_field] $forename invoke type [waitForObject $names::Address_Book_Add_surnameText_text_input_text_field] $surname invoke type [waitForObject $names::Address_Book_Add_emailText_text_input_text_field] $email invoke type [waitForObject $names::Address_Book_Add_phoneText_text_input_text_field] $phone invoke mouseClick [waitForObject $names::Address_Book_Add_OK_button] } }
步骤和Scenario之间的数据共享
让我们向Feature
文件添加一个新的Scenario
。这次我们希望检查的不是通讯录列表中的条目数量,而是列表是否包含正确的数据。因为我们在一个步骤中输入数据,并在另一个步骤中验证它们,所以我们必须在那些步骤之间共享输入数据的信息,以便进行验证。
Scenario: Forename and surname is added to table Given addressbook application is running When I create a new addressbook When I add a new person 'Bob','Doe','[email protected]','123321231' to address book Then previously entered forename and surname shall be at the top
为了共享这些数据,可以使用context.userData。
@When("I add a new person '|word|','|word|','|any|','|integer|' to address book") def step(context, forename, lastname, email, phone): mouseClick(waitForObject(names.editAddButton_button)) type(waitForObject(names.address_Book_Add_forenameText_text_input_text_field), forename) type(waitForObject(names.address_Book_Add_surnameText_text_input_text_field), lastname) type(waitForObject(names.address_Book_Add_emailText_text_input_text_field), email) type(waitForObject(names.address_Book_Add_phoneText_text_input_text_field), phone) mouseClick(waitForObject(names.address_Book_Add_OK_button)) context.userData = {} context.userData['forename'] = forename context.userData['lastname'] = lastname
When("I add a new person '|word|','|word|','|any|','|integer|' to address book") do |context, forename, surname, email, phone| mouseClick(waitForObject(Names::EditAddButton_button)) type(waitForObject(Names::Address_Book_Add_forenameText_text_input_text_field), forename) type(waitForObject(Names::Address_Book_Add_surnameText_text_input_text_field), surname) type(waitForObject(Names::Address_Book_Add_emailText_text_input_text_field), email) type(waitForObject(Names::Address_Book_Add_phoneText_text_input_text_field), phone) mouseClick(waitForObject(Names::Address_Book_Add_OK_button)) context.userData = Hash.new context.userData[:forename] = forename context.userData[:surname] = surname
When("I add a new person '|word|','|word|','|any|','|integer|' to address book", function(context, forename, lastname, email, phone) { mouseClick(waitForObject(names.editAddButtonButton)); type(waitForObject(names.addressBookAddForenameTextTextInputTextField), forename); type(waitForObject(names.addressBookAddSurnameTextTextInputTextField), lastname); type(waitForObject(names.addressBookAddEmailTextTextInputTextField), email); type(waitForObject(names.addressBookAddPhoneTextTextInputTextField), phone); mouseClick(waitForObject(names.addressBookAddOKButton)); context.userData = {}; context.userData['forename'] = forename; context.userData['lastname'] = lastname; });
When("I add a new person '|word|','|word|','|any|','|integer|' to address book", sub { my ($context, $forename, $surname, $email, $phone) = @_; mouseClick(waitForObject($Names::editaddbutton_button)); type(waitForObject($Names::address_book_add_forenametext_text_input_text_field), $forename); type(waitForObject($Names::address_book_add_surnametext_text_input_text_field), $surname); type(waitForObject($Names::address_book_add_emailtext_text_input_text_field), $email); type(waitForObject($Names::address_book_add_phonetext_text_input_text_field), $phone); mouseClick(waitForObject($Names::address_book_add_ok_button)); $context->{userData}{'forename'} = $forename; $context->{userData}{'surname'} = $surname; });
When "I add a new person '|word|','|word|','|any|','|integer|' to address book" {context forename surname email phone} { invoke mouseClick [waitForObject $names::editAddButton_button] invoke type [waitForObject $names::Address_Book_Add_forenameText_text_input_text_field] $forename invoke type [waitForObject $names::Address_Book_Add_surnameText_text_input_text_field] $surname invoke type [waitForObject $names::Address_Book_Add_emailText_text_input_text_field] $email invoke type [waitForObject $names::Address_Book_Add_phoneText_text_input_text_field] $phone invoke mouseClick [waitForObject $names::Address_Book_Add_OK_button] $context userData [dict create forename $forename surname $surname] }
在给定的Feature
的所有Scenario
的所有步骤和Hooks
中都可以访问存储在context.userData中的所有数据。最后,我们需要实现步骤Then previously entered forename and lastname shall be at the top
。
@Then("previously entered forename and surname shall be at the top") def step(context): test.compare(waitForObjectItem(names.address_Book_Unnamed_itemTbl_table_view, '0/0').text, context.userData['forename'], "forename") test.compare(waitForObjectItem(names.address_Book_Unnamed_itemTbl_table_view, '0/1').text, context.userData['lastname'], "lastname")
Then("previously entered forename and surname shall be at the top") do |context| Test.compare(waitForObjectItem(Names::Address_Book_Unnamed_itemTbl_table_view, '0/0').text, context.userData[:forename], "forename"); Test.compare(waitForObjectItem(Names::Address_Book_Unnamed_itemTbl_table_view, '0/1').text, context.userData[:surname], "surname") end
Then("previously entered forename and surname shall be at the top", function(context) { test.compare(waitForObjectItem(names.addressBookUnnamedItemTblTableView, '0/0').text, context.userData['forename'], "forename"); test.compare(waitForObjectItem(names.addressBookUnnamedItemTblTableView, '0/1').text, context.userData['lastname'], "lastname"); });
Then("previously entered forename and surname shall be at the top", sub { my $context = shift; test::compare(waitForObjectItem($Names::address_book_unnamed_itemtbl_table_view, '0/0')->text, $context->{userData}{'forename'}, "forename?"); test::compare(waitForObjectItem($Names::address_book_unnamed_itemtbl_table_view, '0/1')->text, $context->{userData}{'surname'}, "surname?"); });
Then "previously entered forename and surname shall be at the top" {context} { test compare [property get [waitForObjectItem $names::Address_Book_Unnamed_itemTbl_table_view "0/0"] text] [dict get [$context userData] forename] test compare [property get [waitForObjectItem $names::Address_Book_Unnamed_itemTbl_table_view "0/1"] text] [dict get [$context userData] surname] }
Scenario Outline
假设我们的Feature
包含以下两个Scenario
Scenario: State after adding one entry Given addressbook application is running When I create a new addressbook And I add a new person 'John','Doe','[email protected]','500600700' to address book Then '1' entries should be present Scenario: State after adding one entry Given addressbook application is running When I create a new addressbook And I add a new person 'Bob','Koo','[email protected]','500600800' to address book Then '1' entries should be present
如我们所见,这些Scenario
通过使用不同的测试数据执行相同的行为。同样可以通过使用Scenario Outline
(一个包含占位符的Scenario
模板)和示例(一个包含参数的表格)来实现。
Scenario Outline: Adding single entries multiple time Given addressbook application is running When I create a new addressbook And I add a new person '<forename>','<lastname>','<email>','<phone>' to address book Then '1' entries should be present Examples: | forename | lastname | email | phone | | John | Doe | john@m.com | 500600700 | | Bob | Koo | bob@m.com | 500600800 |
请注意,OnScenarioEnd
钩子将在Scenario Outline
的每次循环迭代结束时执行。
测试执行
在Feature
中的所有Scenario
,或者只执行一个选定的Scenario
。为了执行所有Scenario
,必须通过在测试套件视图中点击Play按钮来执行适当的测试用例。
为了只执行一个Scenario
,您需要打开Feature
文件,右键单击给定的Scenario
并选择Run Scenario。另一种方法是单击测试用例资源中Scenario选项卡中相应Scenario
旁边的Play按钮。
Scenario
执行完毕后,Feature
文件将根据执行结果进行着色。更详细的信息(如日志)可以在测试结果视图中找到。
测试调试
Squish提供了在任何点暂停测试用例执行以检查脚本变量、监视应用程序对象或在中运行自定义代码的可能性Squish Script Console。为此,必须在开始执行之前放置一个断点,无论是Feature
文件中的任何包含步骤的行还是正在执行的代码中的任何行(即步骤定义代码的中间)。
当达到断点后,您可以检查所有应用程序对象及其属性。如果在步骤定义或钩子达到断点,则还可以添加验证点或记录代码片段。
重用步骤定义
通过在另一个目录中测试用例中重用步骤定义,可以提高BDD测试的可维护性。更多信息,请参阅 collectStepDefinitions()。
教程:现有测试迁移到BDD
本章面向现有Squish测试用户,他们希望引入行为驱动测试。第一部分描述了如何保留现有测试,仅使用BDD方法创建新测试。第二部分描述了如何将现有的脚本测试用例转换为BDD测试。
将现有测试扩展到BDD
第一种选择是保留任何现有的Squish测试用例,并通过添加新的BDD测试来扩展它们。可以有一个包含基于脚本和BDD测试用例的测试套件。只需打开现有的测试套件,从下拉列表中选择新BDD测试用例选项即可。
假设您的现有测试用例使用了一个库,并且您正在调用共享函数与应用程序进行交互,这些函数也可以在现有的脚本测试用例和新创建的BDD测试用例中使用。以下示例中,一个函数被多个脚本测试用例所使用
def createNewAddressBook():
mouseClick(waitForObject(names.fileNewButton_button))
def createNewAddressBook
mouseClick(waitForObject(Names::FileNewButton_button))
end
function createNewAddressBook(){
mouseClick(waitForObject(names.fileNewButtonButton));
}
sub createNewAddressBook{
mouseClick(waitForObject($Names::filenewbutton_button));
}
proc createNewAddressBook {} { invoke mouseClick [waitForObject $names::fileNewButton_button] }
BDD步骤实现可以轻松使用相同的函数
@When("I create a new addressbook")
def step(context):
createNewAddressBook()
When("I create a new addressbook", function(context){ createNewAddressBook() });
When("I create a new addressbook", sub { createNewAddressBook(); });
When("I create a new addressbook") do |context| createNewAddressBook end
When "I create a new addressbook" {context} {
createNewAddressBook
}
将现有测试转换为BDD
第二种选择是将包含基于脚本的测试的现有测试套件转换为行为驱动测试。由于测试套件可以同时包含脚本测试用例和BDD测试用例,因此迁移可以逐渐进行。一个包含两种测试用例类型的测试套件可以执行并分析结果,而无需额外的努力。
第一步是审查现有测试套件中的所有测试用例,并根据它们所测试的功能
进行分组。每个脚本测试用例将被转换为一个场景
,这是功能
的一部分。例如,假设我们有5个脚本测试用例。审查后,我们发现它们检查了两个功能
。因此,当迁移完成时,我们的测试套件将包含两个BDD测试用例,每个测试用例包含一个功能
。每个功能
将包含多个场景
。在我们的示例中,第一个功能
包含三个场景
,第二个功能
包含两个场景
。
一开始,在包含需要迁移到BDD的Squish测试的功能
的test.feature
文件。接下来,打开test.feature
文件,使用Gherkin语言描述功能
。根据模板的语法,编辑功能
名称,并提供一个简短描述(可选)。接下来,分析将要迁移的脚本测试用例中执行的动作和验证。下面是一个用于addressbook应用程序的示例测试用例
def main(): startApplication("AddressBook.jar") test.log("Create new addressbook") mouseClick(waitForObject(names.fileNewButton_button)) test.compare(waitForObjectExists(names.address_Book_Unnamed_itemTbl_table_view).items.empty, True)
def main startApplication("AddressBook.jar") Test.log("Create new addressbook") mouseClick(waitForObject(Names::FileNewButton_button)) Test.compare(waitForObjectExists(Names::Address_Book_Unnamed_itemTbl_table_view).items.empty, true) end
function main(){ startApplication("AddressBook.jar"); test.log("Create new addressbook"); mouseClick(waitForObject(names.fileNewButtonButton)); test.compare(waitForObjectExists(names.addressBookUnnamedItemTblTableView).items.empty, true); }
sub main { startApplication("AddressBook.jar"); test::log("Create new addressbook"); mouseClick(waitForObject($Names::filenewbutton_button)); test::compare(waitForObjectExists($Names::address_book_unnamed_itemtbl_table_view)->items->empty, 1); }
proc main {} { startApplication "AddressBook.jar" test log "Create new addressbook" invoke mouseClick [waitForObject $names::fileNewButton_button] test compare [property get [property get [waitForObjectExists $names::Address_Book_Unnamed_itemTbl_table_view] items] empty] true }
分析上述测试用例后,我们可以创建以下场景
并将其添加到test.feature
Scenario: Initial state of created address book
Given addressbook application is running
When I create a new addressbook
Then addressbook should have zero entries
接下来,右键单击场景
并从上下文菜单中选择创建缺失步骤实现。这将创建步骤定义的骨架
@Given("addressbook application is running") def step(context): test.warning("TODO implement addressbook application is running") @When("I create a new addressbook") def step(context): test.warning("TODO implement I create a new addressbook") @Then("addressbook should have zero entries") def step(context): test.warning("TODO implement addressbook should have zero entries")
Given("addressbook application is running", function(context) { test.warning("TODO implement addressbook application is running"); }); When("I create a new addressbook", function(context) { test.warning("TODO implement I create a new addressbook"); }); Then("addressbook should have zero entries", function(context) { test.warning("TODO implement addressbook should have zero entries"); });
Given("addressbook application is running", sub { my $context = shift; test::warning("TODO implement addressbook application is running"); }); When("I create a new addressbook", sub { my $context = shift; test::warning("TODO implement I create a new addressbook"); }); Then("addressbook should have zero entries", sub { my $context = shift; test::warning("TODO implement addressbook should have zero entries"); });
Given("addressbook application is running") do |context| Test.warning "TODO implement addressbook application is running" end When("I create a new addressbook") do |context| Test.warning "TODO implement I create a new addressbook" end Then("addressbook should have zero entries") do |context| Test.warning "TODO implement addressbook should have zero entries" end
Given "addressbook application is running" {context} { test warning "TODO implement addressbook application is running" } When "I create a new addressbook" {context} { test warning "TODO implement I create a new addressbook" } Then "addressbook should have zero entries" {context} { test warning "TODO implement addressbook should have zero entries" }
现在我们将脚本测试用例中的代码片段放入相应的步骤定义中,并删除包含test.warning
的行。如果您的脚本测试用例使用共享脚本,您也可以在步骤定义中调用这些函数。例如,最终结果可能如下所示
@Given("addressbook application is running") def step(context): startApplication("AddressBook.jar") stage = waitForObject(names.address_Book_Stage) test.compare(stage.showing, True) @When("I create a new addressbook") def step(context): mouseClick(waitForObject(names.fileNewButton_button)) @Then("addressbook should have zero entries") def step(context): test.compare(waitForObjectExists(names.address_Book_Unnamed_itemTbl_table_view).items.empty, True)
Given("addressbook application is running") do |context| startApplication("AddressBook.jar") stage = waitForObject(Names::Address_Book_Stage) Test.compare(stage.showing, true) end When("I create a new addressbook") do |context| mouseClick(waitForObject(Names::FileNewButton_button)) end Then("addressbook should have zero entries") do |context| Test.compare(waitForObjectExists(Names::Address_Book_Unnamed_itemTbl_table_view).items.empty, true) end
Given("addressbook application is running", function(context) { startApplication("AddressBook.jar"); var stage = waitForObject(names.addressBookStage); test.compare(stage.showing, true); }); When("I create a new addressbook", function(context) { mouseClick(waitForObject(names.fileNewButtonButton)); }); Then("addressbook should have zero entries", function(context) { test.compare(waitForObjectExists(names.addressBookUnnamedItemTblTableView).items.empty, true); });
Given("addressbook application is running", sub { my $context = shift; startApplication("AddressBook.jar"); my $stage = waitForObject($Names::address_book_stage); test::compare($stage->showing, 1); }); When("I create a new addressbook", sub { my $context = shift; mouseClick(waitForObject($Names::filenewbutton_button)); }); Then("addressbook should have zero entries", sub { my $context = shift; test::compare(waitForObjectExists($Names::address_book_unnamed_itemtbl_table_view)->items->empty, 1); });
Given "addressbook application is running" {context} { startApplication "AddressBook.jar" set stage [waitForObject $names::Address_Book_Stage] test compare [property get $stage showing] true } When "I create a new addressbook" {context} { invoke mouseClick [waitForObject $names::fileNewButton_button] } Then "addressbook should have zero entries" {context} { test compare [property get [property get [waitForObjectExists $names::Address_Book_Unnamed_itemTbl_table_view] items] empty] true }
请注意,在将此基于脚本的测试迁移到BDD时,删除了test.log("Create new addressbook")
。当执行步骤我创建一个新的地址簿
时,步骤名称将记录到测试结果中,因此test.log
调用将是多余的。
©2024 The Qt Company Ltd. 本文档中的文档贡献是各自所有者的版权。
提供的文档根据GNU自由文档许可证版本1.3的条款颁发,由自由软件基金会发布。
Qt及其相关标志是Finland和/或其他国家的The Qt Company Ltd.的商标。所有其他商标均为各自所有者的财产。