JavaFX Squish BDD 指南

教程:设计行为驱动开发(BDD)测试

本教程将向您展示如何为示例应用程序创建、运行和修改行为驱动开发(BDD)测试。您将了解Squish最常用的功能。完成本教程后,您将能够为自己的应用程序编写测试。

注意:如果需要视频指导,可以在Qt Academy的Qt Academy找到关于Squish行为驱动测试的40分钟在线课程

在本章中,我们将使用用JavaFX编写的简单地址簿应用程序作为我们测试的应用程序(AUT)。这是一个非常基本的应用程序,允许用户加载现有的地址簿或创建一个新的地址簿,添加、编辑和删除条目。截图显示了用户正在添加一个新名称和地址的应用程序。

"The JavaFX \c {addressbook} example"

行为驱动开发(BDD)简介

行为驱动开发(BDD)是测试驱动开发(TDD)的扩展,它将验收标准定义放在开发过程的开端,而不是在软件开发完成后编写测试。测试后的代码更改可能有循环。

"BDD process"

行为驱动测试由一系列功能文件组成,这些文件通过一个或多个场景描述产品的功能,这些场景定义了预期的应用程序行为。每个场景都是一系列步骤的集合,这些步骤代表需要为该场景测试的动作或验证。

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测试用例”设置为默认。

"Creating new BDD Test Case"

新创建的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 )。

"Record Scenario"

这将导致Squish运行AUT,以便我们可以与之交互。此外,会显示控制栏,其中列出了所有需要记录的步骤。现在所有与AUT的交互或添加到脚本中的任何验证点都将记录在第一个步骤Given addressbook application is running下(该步骤在控制栏的步骤列表中被加粗)。为了验证这个先决条件是否满足,我们将添加一个验证点。要做到这一点,点击控制栏上的Verify并选择Properties

"Control Bar"

结果,squishide进入Spy模式,显示所有应用程序对象及其属性。在选择Properties View中,选中“showing”属性的复选框。最后,点击Save and Insert Verifications按钮。 squishide消失,控制栏再次显示。

"Inserting Verification Point"

当我们完成每个步骤后,我们可以通过点击控制栏左侧当前步骤旁边的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(在我们的例子中,这个表为空)。

"Up and Picker tool Buttons"

展开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具有不同类型的参数,如anyintegerword,允许我们的步骤定义更具可重用性。让我们在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 bookThen '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按钮来执行适当的测试用例。

"Execute all Scenarios from Feature"

为了只执行一个Scenario,您需要打开Feature文件,右键单击给定的Scenario并选择Run Scenario。另一种方法是单击测试用例资源中Scenario选项卡中相应Scenario旁边的Play按钮。

"Execute one Scenario from Feature"

Scenario执行完毕后,Feature文件将根据执行结果进行着色。更详细的信息(如日志)可以在测试结果视图中找到。

"Execution results in Feature file"

测试调试

Squish提供了在任何点暂停测试用例执行以检查脚本变量、监视应用程序对象或在中运行自定义代码的可能性Squish Script Console。为此,必须在开始执行之前放置一个断点,无论是Feature文件中的任何包含步骤的行还是正在执行的代码中的任何行(即步骤定义代码的中间)。

"Breakpoint in Feature file"

当达到断点后,您可以检查所有应用程序对象及其属性。如果在步骤定义或钩子达到断点,则还可以添加验证点或记录代码片段。

重用步骤定义

通过在另一个目录中测试用例中重用步骤定义,可以提高BDD测试的可维护性。更多信息,请参阅 collectStepDefinitions()

教程:现有测试迁移到BDD

本章面向现有Squish测试用户,他们希望引入行为驱动测试。第一部分描述了如何保留现有测试,仅使用BDD方法创建新测试。第二部分描述了如何将现有的脚本测试用例转换为BDD测试。

将现有测试扩展到BDD

第一种选择是保留任何现有的Squish测试用例,并通过添加新的BDD测试来扩展它们。可以有一个包含基于脚本和BDD测试用例的测试套件。只需打开现有的测试套件,从下拉列表中选择新BDD测试用例选项即可。

"Creating new BDD Test Case"

假设您的现有测试用例使用了一个库,并且您正在调用共享函数与应用程序进行交互,这些函数也可以在现有的脚本测试用例和新创建的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测试用例,每个测试用例包含一个功能。每个功能将包含多个场景。在我们的示例中,第一个功能包含三个场景,第二个功能包含两个场景

"Conversion Chart"

一开始,在包含需要迁移到BDD的Squish测试的中打开一个测试套件。接下来,通过在下拉菜单中选择新BDD测试用例选项来创建一个新的测试用例。每个BDD测试用例包含一个可以填充最多一个功能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调用将是多余的。

以上示例在此教程中已简化。为了充分利用Squish中的行为驱动测试,请熟悉行为驱动测试部分以及API参考

©2024 The Qt Company Ltd. 本文档中的文档贡献是各自所有者的版权。
提供的文档根据GNU自由文档许可证版本1.3的条款颁发,由自由软件基金会发布。
Qt及其相关标志是Finland和/或其他国家的The Qt Company Ltd.的商标。所有其他商标均为各自所有者的财产。