如何创建和使用共享数据和共享脚本

本节讨论如何将测试拆分为多个文件,以及如何共享、访问和使用共享脚本和共享数据文件。(还可以共享对象映射;有关详细信息,请参阅创建对象映射。)

注意:如果在Qt Academy想要一些视频指导,可以观看Squish数据驱动测试的1小时在线课程Qt Academy

当您拥有多个测试用例共同的功能时,共享脚本是非常理想的。然而,如果您一般是从头创建测试脚本,且它们都有相似的基本结构,创建测试脚本模板可能更方便,这样可以节省每次创建新测试时复制粘贴的麻烦:请参考测试用例模板了解如何实现。

如何存储和定位共享脚本和共享数据文件

每个测试用例都包含一个默认的测试脚本文件,文件名为test.js(JavaScript)、test.py(Python)、test.pl(Perl)、test.rb(Ruby)或test.tcl(Tcl),具体取决于为测试套件设置的脚本语言。

测试套件中的测试用例通常需要一些公共功能或公共数据。Squish允许您创建包含测试用例可以共享的公共功能的单独脚本文件。此外,Squish还允许您创建或导入用于共享的测试数据。

对于测试数据,Squish提供两种选项:与特定测试用例相关的测试数据以及可以由测试套件中的任何测试用例共享的测试数据。对于测试脚本,Squish提供三种选项:特定于某个测试用例的测试脚本(除了test.*文件外)、可以由测试套件中的任何测试用例共享的测试脚本以及可以由任何测试套件中的任何测试用例共享的全局测试脚本。全局脚本是一个高级功能,不建议新用户使用。

为了简化解释,假设我们有一个名为myapp_suite的测试套件。进一步假设,在该套件中,我们有两个测试用例分别位于myapp_suite/tst_case1myapp_suite/tst_case2文件夹中。如果我们使用Python脚本语言,第一个测试用例的文件名将是myapp_suite/tst_case1/test.py,其他脚本语言也是如此。

基于以上假设,如果我们想要为第一个测试用例存储一些特定的测试用例数据,它必须存储在myapp_suite/tst_case1/testdata文件夹中,例如myapp_suite/tst_case1/testdata/case1_data.csv。如果我们想要存储可以被两个测试用例(以及后来创建的其他测试用例)共享的测试数据,它必须存储在myapp_suite/shared/testdata文件夹中。这些细节对于命令行用户来说很重要,但如果您使用squishide,则无需记住这些,因为squishide可以用于创建或导入测试数据,并会将其存储在正确位置,在测试脚本中,使用文件名(而不是路径)来访问测试数据。

无论测试数据是针对特定测试用例还是由整个测试套件共享,我们始终采用相同的 技术 来访问数据:我们使用测试数据的文件名(不含路径)调用Dataset testData.dataset(filename)函数,以获取可使用其他 测试数据函数函数访问数据的数组或元组(取决于脚本语言)。(也可以通过提供带有路径的文件名在 任何位置 访问测试数据,但我们建议将测试数据存储在相关的测试套件或测试用例中。)

如果我们想要一个或多个额外的脚本以整洁的模块化功能性,或者简化我们的test.*测试脚本,我们可以创建额外的脚本文件。根据前面的假设,如果我们想要一个额外的特定测试用例的脚本,我们必须将其放在与test.*测试脚本相同的目录中,例如,myapp_suite/tst_case1/extra.py。如果我们希望额外的功能可以被我们的整个测试套件的测试脚本访问,我们必须将额外的脚本放在测试套件共享脚本文件夹中,例如,myapp_suite/shared/scripts/common.py。如果我们希望在测试套件的任何测试用例中共享功能,我们可以在全局脚本视图中添加全局测试脚本。

无论测试脚本是否针对特定测试用例、由整个测试套件共享还是对所有测试套件都是全局的,我们始终采用相同的技术来访问脚本的功能:我们调用source(filename)函数并传入脚本的名字(包括其路径)。通常,我们通过调用String findFile(where, filename)函数来获取这个名字,给它提供一个首参数为"scripts",第二个参数是脚本的名称(不含路径)——这将对特定测试用例的脚本和任何测试套件测试用例共享的脚本有效,也适用于全局共享的脚本。请注意,在Ruby上,我们使用require函数而不是source

要创建可以由测试套件中的任何测试用例共享的脚本,在"squishide"的测试套件视图的测试套件资源部分,点击新建测试资源按钮。这将创建一个新的空共享脚本,命名为script_1.py(或script_1.js等,取决于所使用的脚本语言)。将脚本重命名为更有意义的名字,可以立即输入新名字,或者稍后通过点击它,然后在上下文菜单中选择重命名选项。在截图中,我们将共享脚本重命名为common.py,因为它包含通用功能。

{}

测试套件视图中的新建测试资源按钮

一旦共享脚本被重命名,点击(或根据您的平台和设置双击)以在编辑器视图中显示它。您现在可以编辑脚本,以添加所需的任何共享功能。

还可以(尽管不太常见)添加仅供特定测试用例使用的共享脚本。为此,在测试套件视图的测试用例资源部分,点击新建测试资源工具栏按钮。您可以向测试套件和测试用例添加无限数量的脚本文件。

您可以在全局脚本视图中创建全局共享脚本,即可以被测试套件中的任何测试用例访问的脚本。

如果要将现有脚本导入测试套件的共享脚本文件夹(或测试用例的共享脚本文件夹),请点击文件 > 导入测试资源。这将弹出导入Squish资源对话框。输入要导入的脚本文件的名称(或使用浏览按钮选择它)。然后将导入为组合框项设置为"脚本"。如果想让脚本由测试套件的所有测试用例共享(而不仅仅是当前显示的测试用例),请勾选在测试套件中创建共享文件的单选按钮。这建议这样做,因为通常在所有测试用例之间共享脚本会更方便。现在单击完成按钮,导入的脚本将可在squishide中使用。

可以创建共享脚本而不使用squishide,这是可能的。首先,在控制台中,将目录更改为测试套件的目录。然后,如果不存在,创建一个名为shared的新子目录。现在进入此目录,如果不存在,创建一个名为scripts的新子目录。现在使用您的编辑器或IDE创建共享脚本,并确保将其保存在测试套件的shared/scripts目录中。如果您已经有了脚本,只需将其复制到该目录即可。如果想要添加全局共享脚本,只需创建或复制到您想要使用其用的目录(或其中一个目录)。

可能存在这样的情况,共享功能不仅在一套特定的测试套件中可能有用,而且在大多数或所有测试套件中都有用。Squish使所有测试套件都能访问全局共享脚本。这是通过创建一个或多个目录(任何您喜欢的地方)来实现的,您将所有全局共享脚本放进去。然后,为了让Squish知道存在这样的目录,您可以使用全局脚本视图(这会将它们添加到paths.ini 初始化文件中),或将环境变量SQUISH_SCRIPT_DIR设置为一个或多个路径。例如,在Unix中使用bash shell,export SQUISH_SCRIPT_DIR=$HOME/tests/shared_scripts。查看环境变量为AUT设置环境变量

一旦创建了共享脚本,它就可以被任何需要它的测试用例特定脚本所使用。注意,通常不使用特定语言的导入机制来导入共享脚本——因为毕竟并不是所有语言都有这样的机制,例如,JavaScript就没有。相反,Squish API提供了必要的函数。

定位共享脚本文件(或共享数据文件)的标准方式是使用String findFile(where, filename)函数。第一个参数是文件的类型,对于共享脚本应该是"脚本"。第二个参数是脚本的文件名(不带路径)。该函数将搜索所有Squish使用的标准位置,并返回包含完整路径的文件名。

一旦我们有了共享脚本的完整路径,我们就可以将其包含在我们的测试用例脚本中。这是通过使用source(filename)函数来评估共享脚本完成的——这意味着它实际上是在调用source(filename)函数的位置处以共享脚本的实际文本作为测试用例执行的效果。之后,所有在共享脚本中创建的对象(通常是类和函数)都可以在测试用例脚本中使用。

以下是一个示例,其中我们想要共享一个名为address_utility.py(或address_utility.js等,具体取决于正在使用的脚本语言),以便我们可以访问其中的一个函数——在这个例子中是insertDummyNamesAndAddresses函数——它向地址簿AUT添加一些名称和地址,以便进行后续测试。

def main():
    ...
    source(findFile("scripts", "address_utility.py"))
    insertDummyNamesAndAddresses()
    ...
function main()
{
    // ...
    source(findFile("scripts", "address_utility.js"));
    insertDummyNamesAndAddresses();
    // ...
}
sub main
{
    # ...
    source(findFile("scripts", "address_utility.pl"));
    insertDummyNamesAndAddresses();
    # ...
}
# encoding: UTF-8
require 'squish'
include Squish

def main
    # ...
    require findFile("scripts", "address_utility.rb")
    insertDummyNamesAndAddresses
    # ...
end
proc main {} {
    # ...
    source [findFile scripts "address_utility.tcl"]
    insertDummyNamesAndAddresses
    # ...
}

在此,我们导入名为address_utility.py(或类似)的脚本,该脚本定义了一个名为insertDummyNamesAndAddresses的函数,然后我们可以调用它。

对于支持导入的脚本语言,如Python,可以使用语言的标准导入机制。但是Squish的方法通常更方便,因为大多数情况下我们共享的脚本只与我们的测试相关。通常,对于共享脚本,最好使用Squish的source(filename)函数,但对于导入标准模块,最好使用特定于语言的机制(例如,Python中的import和Perl中的use)。在Ruby中,使用标准require函数而不是Squish的source函数。

就像我们有一个测试用例脚本一样,我们还可以有特定的测试用例数据文件。这些文件存储在测试用例的testdata目录中。然而,在某些情况下,我们想要共享测试数据以便多个测试用例可以访问它。在这种情况下,我们将测试数据存储在测试套件的shared/testdata目录中。

可以通过squishide导入文件来添加测试数据——Squish可以读取.tsv(制表符分隔值格式)、.csv(逗号分隔值格式)、.xls.xlsx(Microsoft Excel电子表格格式)。或者我们可以在命令行中简单地在控制台创建目录并将我们的测试数据复制到它们中。用于添加共享测试数据的技术,无论是使用squishide还是手动,都与添加共享测试脚本的完全相同,只是我们使用适当的testdata目录(而不是scripts目录)。

尽管顶层目录结构必须遵循我们所述的结构,但在该结构within——即在testdata目录下面——您可以自由地创建子目录,并以您喜欢的方式组织它们。

使用我们前面提到的String findFile(where, filename)函数获取测试数据,此时第一个参数必须是"testdata",第二个参数是您想要访问的测试数据文件名称。在实践中,我们通常不需要使用String findFile(where, filename)函数来获取测试数据。相反,我们使用Dataset testData.dataset(filename)函数,并使用Squish的Test Data Functions API访问数据,如我们将在下一节中看到。

如何进行数据驱动测试

数据驱动测试是一种将测试数据(输入和预期输出)与只包含测试逻辑的测试用例代码分开的方法。常规做法是从文件或数据库中一次读取一个或一条测试数据,测试用例使用这些数据来测试AUT,然后比较其结果与预期结果。

这种方法的优点之一是可以在不实际更改测试用例代码的情况下修改测试——我们只需要添加更多测试可以读取和处理的数据。这使得在拥有编码技能的测试工程师和无编码技能的测试工程师之间分离创建测试的责任成为可能。拥有编码技能的人可以创建测试脚本并在其中编码测试逻辑,而没有编码技能的测试工程师可以创建和编辑测试脚本用于测试AUT的测试数据。

Squish提供了一个用于处理测试数据的API(见测试数据函数),这使得创建数据驱动的测试变得容易。在这里,我们将探讨如何使用Squish的脚本API读取和使用测试数据,并假设测试数据已经导入或复制到适当的testdata目录中。

测试数据始终以表格形式包含数据。Squish可以读取.tsv(制表符分隔值格式)、.csv(逗号分隔值格式)、.xls.xlsx(Microsoft Excel表格格式)格式的文件。对于.csv.tsv文件,Squish假定它们使用Unicode UTF-8编码——与测试脚本相同的编码。在.tsv文件中,使用新行分隔记录,使用制表符分隔字段。第一条记录用于描述列。以下是一个包含制表符由"\t"表示,换行符由"\n"表示的示例.tsv数据文件——addresses.tsv

First Name\tLast Name\tAddress\tEmail\tNumber\n
Max\tMustermann\tBakerstreet 55\tmax@mustermann.net\t1\n
John\tKelly\tRhodeo Drv. 678\tjkelly@acompany.com\t2\n
Joe\tSmith\tQueens Blvd. 37\tjoe@smith.com\t3\n

每个字段(列)由一个制表符分隔,每个记录(行或行)由一个换行符分隔。正如.tsv(和.csv)文件中的常见做法,第一行并不是实际的数据,而是字段(列)名称("名字","姓氏"等)。所有其他行都是数据记录。

以下是一个示例,我们逐个读取记录并打印其值到Squish的日志中

for record in testData.dataset("addresses.tsv"):
    firstName = testData.field(record, "First Name")
    lastName = testData.field(record, "Last Name")
    address = testData.field(record, "Address")
    email = testData.field(record, "Email")
    test.log("%s %s, %s; email: %s" % (
        firstName, lastName, address, email))
var records = testData.dataset("addresses.tsv");
for (var row = 0; row < records.length; ++row) {
    var record = records[row];
    firstName = testData.field(record, "First Name");
    lastName = testData.field(record, "Last Name");
    address = testData.field(record, "Address");
    email = testData.field(record, "Email");
    test.log(firstName + " " + lastName + ", " + address +
        "; email:" + email);
}
my @records = testData::dataset("addresses.tsv");
for (my $row = 0; $row < scalar(@records); $row++) {
    my $record = $records[$row];
    my $firstName = testData::field($record, "First Name");
    my $lastName = testData::field($record, "Last Name");
    my $address = testData::field($record, "Address");
    my $email = testData::field($record, "Email");
    test::log("$firstName $lastName, $address; email: $email");
}
TestData.dataset("addresses.tsv").each do |record, row|
    firstName = TestData.field(record, "First Name")
    lastName = TestData.field(record, "Last Name")
    address = TestData.field(record, "Address")
    email = TestData.field(record, "Email")
    Test.log("#{firstName} #{lastName}, #{address}; email: #{email}")
end
set records [testData dataset "addresses.tsv"]
for {set row 0} {$row < [llength $data]} {incr row} {
    set record [lindex $records $row]
    set firstName [testData field $record "First Name"]
    set lastName [testData field $record "Last Name"]
    set address [testData field $record "Address"]
    set email [testData field $record "Email"]
    test log "$firstName $lastName, $address; email: $email"
}

请注意,我们通过名称访问字段,因此我们在测试用例代码中使用的名称必须与测试数据文件的第一行中的名称匹配。另外,尽管数据在末尾有一个"数字"字段,但我们忽略它,因为我们不需要它。

在典型情况下,可以通过Dataset testData.dataset(filename)函数找到测试数据文件,该函数在标准位置中搜索测试数据,并返回一个记录数组或元组。(也可以给这个函数提供一个带有路径的文件名——例如,通过String findFile(where, filename)函数返回的)。然后我们使用String testData.field(record, fieldName)函数访问给定记录中的单个字段内容。

使用for循环,我们可以遍历测试数据中的每个记录,而无需事先知道记录的数量,因此代码不受记录被删除或添加的影响。当然,在现实世界的测试中,我们会将数据馈送到AUT并比较预期结果与实际结果,而不仅仅是像我们这里那样将数据打印到日志中。

大多数教程都包括一个数据驱动测试的完整示例。对于Qt示例,请参阅创建数据驱动测试;对于Java AWT/Swing示例,请参阅创建数据驱动测试;对于Java SWT示例,请参阅创建数据驱动测试;以及对于Tk/Tcl示例,请参阅创建数据驱动测试

如何使用测试数据在应用-under-test(AUT)中

到目前为止,这一章节仅讨论了在测试脚本中使用测试数据创建数据驱动测试。但在实践中还出现了两种其他用例。一种是用例是提供测试数据文件供AUT读取,另一种是我们希望在测试运行期间检索AUT创建的文件,以便进行验证。

让我们先来看第一个用例,其中我们为AUT提供一个数据文件进行读取。例如,假设我们使用的是addressbook AUT,我们希望在测试脚本开始时使它加载一个名为customers.adr的文件,以便它有一组已知的数据来处理。这通过将数据文件存储在测试用例的目录中,或者其testdata目录中,或者如果我们想让多个测试用例都能访问它,可以在测试套件的shared/testdata目录中实现。

我们希望在测试脚本中避免硬编码数据文件的路径,因为我们希望灵活地在不同的机器上,甚至在不同平台上运行我们的测试。我们可以使用< a href="squish-api.html#testdata-put-function" translate="no">testData.put(filename) 函数将数据文件复制到AUT的当前工作目录中(而无需知道其路径)。我们只需向此函数提供文件名即可,因为它将自动在测试用例的目录中查找。如果我们想从测试用例的testdata目录或从测试套件的shared/testdata目录中放置文件,我们可以使用String findFile(where, filename) 函数的返回结果调用< a href="squish-api.html#testdata-put-function" translate="no">testData.put(filename) 函数,给后者一个"testdata"的首个参数和文件名的第二个参数(不包含路径)。使用testData.put(filename) 函数的另一个好处是,一旦测试运行结束,Squish将自动为我们清理(即,Squish将删除AUT工作目录中的文件)。

以下是一个示例,我们将测试数据文件从当前测试用例的目录复制到AUT的工作目录。然后我们访问AUT的主窗口对象( Addressbook),并调用该对象的fileOpen 方法来加载我们想要其加载的文件名。

def main():
    ...
    testData.put("customers.adr")
    findObject("Addressbook").fileOpen("customers.adr")
    ...
function main()
{
    // ...
    testData.put("customers.adr");
    findObject("Addressbook").fileOpen("customers.adr");
    // ...
}
sub main()
{
    # ...
    testData::put("customers.adr");
    findObject("Addressbook")->fileOpen("customers.adr");
    # ...
}
# encoding: UTF-8
require 'squish'
include Squish

def main
    #...
    TestData.put("customers.adr")
    findObject("Addressbook").fileOpen("customers.adr")
    #...
end
proc main {} {
    # ...
    invoke testData put "customers.adr"
    [invoke [invoke findObject "Addressbook"] fileOpen "customers.adr"]
    # ...
}

另一个用例是,我们需要验证由AUT创建的文件具有我们预期的内容。例如,假设在测试期间, addressbook AUT加载一个数据文件, customers.adr,执行各种操作(增加、编辑和删除地址),然后将当前地址数据保存到一个新的文件中, edited-customers.adr。在这之后,我们希望我们的测试脚本将 edited-customers.adr 文件与我们期望文件在脚本前期所做的数据更改后来具有的内容进行比较的另一个文件, expected-customers.adr

我们可以使用testData.put(filename) 函数将文件复制到AUT的当前目录中。以下是一个示例,它将文件保存到AUT的当前工作目录中,将“期望”文件复制到AUT的当前工作目录中,然后读取这两个文件并将它们进行比较以查看是否匹配。

def main():
    # Load customers.adr and add/edit/delete addresses
    ...
    mainwindow = waitForObject("Addressbook")
    # Use the AUT's API to save the data to the AUT's directory
    mainwindow.saveAs("edited-customers.adr")

    # Copy "expected" from the AUT's directory to the test case's directory
    testData.get("expected-customers.adr")

    edited = open("edited-customers.adr").read()
    expected = open("expected-customers.adr").read()
    test.compare(edited, expected)
function main()
{
    // Load customers.adr and add/edit/delete addresses
    ...
    var mainwindow = waitForObject("Addressbook");
    // Use the AUT's API to save the data to the AUT's directory
    mainwindow.saveAs("edited-customers.adr");

    // Copy "expected" from the AUT's directory to the test case's directory
    testData.get("expected-customers.adr");

    var edited = File.open("edited-customers.adr").read();
    var expected = File.open("expected-customers.adr").read();
    test.compare(edited, expected);
}
sub main
{
    # Load customers.adr and add/edit/delete addresses
    ...
    my $mainwindow = waitForObject("Addressbook");
    # Use the AUT's API to save the data to the AUT's directory
    $mainwindow->saveAs("edited-customers.adr");

    # Copy "expected" from the AUT's directory to the test case's directory
    testData::get("expected-customers.adr");

    open(FH1, "edited-customers.adr");
    open(FH2, "expected-customers.adr");
    my $edited = join("", <F1>);
    my $expected = join("", <F2>);
    test.compare($edited, $expected);
}
# encoding: UTF-8
require 'squish'
include Squish

def main
    # Load customers.adr and add/edit/delete addresses
    # ...
    mainwindow = waitForObject("Addressbook")
    # Use the AUT's API to save the data to the AUT's directory
    mainwindow.saveAs("edited-customers.adr")

    # Copy "expected" from the AUT's directory to the test case's directory
    TestData.get("expected-customers.adr")

    edited = open("edited-customers.adr").read
    expected = open("expected-customers.adr").read
    Test.compare(edited, expected)
end
proc main {} {
    # Load customers.adr and add/edit/delete addresses
    ...
    set mainwindow [waitForObject "Addressbook"]
    # Use the AUT's API to save the data to the AUT's working directory
    invoke $mainwindow saveAs "edited-customers.adr"

    # Copy "expected" from the AUT's directory to the test case's directory
    testData get "expected-customers.adr"

    set edited [read [open "edited-customers.adr"]]
    set expected [read [open "expected-customers.adr"]]
    test compare $edited $expected
}

我们首先获取AUT的主窗口对象引用;然后调用主窗口的saveAs方法来保存编辑后的数据。然后打开保存后的新数据文件和预期数据文件(后者已复制到AUT的工作目录),并读取它们的全部内容。最后,我们使用布尔值比较函数 名称.value1,value2比较两个文件的全部内容。

Squish的数据处理API(见测试数据函数)包含其他有用的函数。例如,如果我们只想测试文件是否创建,而不关心其内容,我们可以调用布尔值函数 testData.exists(filename)。如果我们想在测试过程中删除文件,可以调用函数 testData.remove(filename)

©2024年 Qt公司有限公司。在此文档中的文档贡献版权属各自所有者。
提供的文档适用于GNU自由文档许可协议 версии 1.3 的条款,由Free Software Foundation发布。
Qt及其相关标志是芬兰和/或其他国家/地区的The Qt Company Ltd的商标。所有其他商标均属其各自所有者的财产。