如何测试Java应用程序

Squish的Java特定API使您能够查找和查询对象,以及访问属性和方法

此外,Java 便利 API提供了执行 GUI 应用程序中常见操作的功能,例如点击按钮或输入一些文本。

有关如何使用脚本 Java API访问和测试复杂 Java GUI 元素(包括列表、表格和树小部件)的示例,请参阅如何测试应用程序。为 AWT/Swing 和 SWT 应用程序提供了独立的示例,尽管相同的原则适用于两者。

Java 属性

在这种情况下,属性表示 Java 中的字段。也就是说,具有遵循特定命名方案的方法的类,例如

SomeType getSomething();
boolean isSomething();
void setSomething(SomeType someValue);

当 Squish 看到名为 getXyzisXyz 的方法时,会创建一个名为 xyz 的属性。除非存在名为 setXyz 的方法,否则属性为只读。Squish 从不创建只写属性,因此如果只存在设置器,则将其视为普通方法。在上面的例子中,(假设仅定义了 getSomethingisSomething 中的一个),Squish 将创建一个名为 something 的属性。

如何查找和查询 Java 对象

函数 Object waitForObject(objectOrName) 当对象名称作为一个有资格对象名称可用时,即它变得可见时,返回该对象。对于隐藏的对象,请改用函数Object findObject(objectName)

Squish 支持三种标识对象的名称的符号

  • 符号名称—这些名称是通过算法生成的,并在 Squish 对象图 中使用,以使得针对 AUT 对象层次结构变化的测试更容易创建。这些名称类似于层次名称,因为它们以冒号开始,由一个或多个点分隔的文本组成——例如,:Payment Form.Pay_javax.swing.JButton。符号名称适用于测试脚本(而且是 Squish 在录制脚本时使用的),因为它们使得测试脚本维护更容易。有关处理对象层次结构变化的信息,请参见编辑对象图
  • 多属性(真实)名称—以括号内的属性名称=value 等对的列表唯一标识对象。Squish 将在 GUI 父母-子继承关系中搜索,直到找到匹配的对象。以下是一个这样的名称的示例:{caption='Pay' type='javax.swing.JButton' visible='true' window=':Payment Form_PaymentForm'} 。为了大多数 GUI 工具包的合法性,多属性名称(也称为“真实名称”)必须包括一个 type 属性。但是,Squish for Web 和 Squish for Windows 不需要指定 type 属性——如果存在当然会使用它。注意,在这个例子中还引用了另一个对象(即 window);在这种情况下,该参考使用了符号名称。通常,使用符号名称更健壮,但如果我们需要用可变属性标识对象(例如,标题可能更改的标题),那么我们必须使用多属性名称,因为这种命名方案支持通配符。(有关通配符的更多信息,请参阅 改进对象识别。)
  • 层次名称—从顶部框架(或SWT中的外壳)开始,到对象的所有父级GUI元素的“路径”,每个元素之间用句点分隔。以下是一个示例名称::frame0.JRootPane.null_layeredPane.null_contentPane.JLabel。这些名称为向后兼容而支持,但不应该用于新测试。

要找到对象名称,可以使用Spy工具来检查自动测试系统(AUT)。有关详情,请参阅如何使用Spy部分。

在测试中使用真实名称和符号名称都是完全可以的。最常见的情况是使用符号名称(通常从对象映射或从记录的脚本中剪切并粘贴),并且只有在需要通配符功能时才使用真实名称。

要获取对象的引用,您可以使用符号名称或真实的多属性(多属性)名称,甚至是层次名称。将名称传递给Object waitForObject(objectOrName)函数。例如:

forenameTextField = waitForObject(":Address Book - " +
    "Add.Forename:_javax.swing.JTextField")

可以使用四种基本语法定义来访问对象。第一种是简单地使用Object waitForObject(objectOrName)函数,如下所示。对于预期会看到的对象,这非常适合大多数情况。对于对象可能看不见(例如,在一个未显示的选项卡小部件上的对象)或甚至可能不存在(例如,只有在某些情况下由AUT创建的对象)的情况,我们可以采用三种方法之一,具体取决于我们的需求。

如果我们预期对象存在并可见,但又要考虑它偶尔不可见的情况,我们可以使用以下代码:

try:
    textField = waitForObject(
        ":Credit Card.Account Name:_javax.swing.JTextField")
    # here we can use the textField object reference
except LookupError, err:
    test.fail("Could not find the account name text field")
try {
    var textField = waitForObject(
        ":Credit Card.Account Name:_javax.swing.JTextField");
    // here we can use the textField object reference
} catch(err) {
    test.fail("Could not find the account name text field");
}
eval {
    my $textField = waitForObject(
        ":Credit Card.Account Name:_javax.swing.JTextField");
    # here we can use the textField object reference
} or do {
    test::fail("Could not find the account name text field");
};
begin
    textField = waitForObject(
        ":Credit Card.Account Name:_javax.swing.JTextField")
    # here we can use the textField object reference
rescue LookupError => err
    Test.fail("Could not find the account name text field")
end
catch {
    set textField [waitForObject \
        ":Credit Card.Account Name:_javax.swing.JTextField"]
    # here we can use the textField object reference
} result options
if {[dict get $options -code]} {
    test fail "Could not find the account name text field"
}

如果预期一个对象是缺失的(例如,在某些情况下应该消失的按钮),我们可以这样检查:

code = ('waitForObject(":Credit ' +
        'Card.Account Name:_javax.swing.JTextField")')
test.exception(code, "Correctly didn't find the text field")
var code = 'waitForObject(":Credit ' +
        'Card.Account Name:_javax.swing.JTextField")';
test.exception(code, "Correctly didn't find the text field");
my $code = 'waitForObject(":Credit ' .
        'Card.Account Name:_javax.swing.JTextField")';
test::exception($code, "Correctly didn't find the text field");
code = 'waitForObject(":Credit ' +
       'Card.Account Name:_javax.swing.JTextField")'
Test.exception(code, "Correctly didn't find the text field")
set code = {[waitForObject \
        ":Credit Card.Account Name:_javax.swing.JTextField"]}
test exception $code "Correctly didn't find the text field"

Boolean test.exception(code)函数执行给定的代码,并期望代码抛出异常。

如果我们预期一个对象是隐藏的,但实际上存在(例如,在一个不像当前页面的选项卡页上的对象),我们仍然可以访问它,但是这次不能使用Object waitForObject(objectOrName)函数——该函数仅适用于可见对象——而必须联用Boolean object.exists(objectName)函数和Object findObject(objectName)函数。

if object.exists(":Credit Card.Account Name:_javax.swing.JTextField"):
    textField = findObject(
        ":Credit Card.Account Name:_javax.swing.JTextField")
    if textField:
    test.passes("Correctly found the hidden object")
if (object.exists(":Credit Card.Account Name:_javax.swing.JTextField")) {
    var textField = findObject(
        ":Credit Card.Account Name:_javax.swing.JTextField");
    if (textField)
    test.pass("Correctly found the hidden object");
}
if (object::exists(":Credit Card.Account Name:_javax.swing.JTextField")) {
    my $textField = findObject(
        ":Credit Card.Account Name:_javax.swing.JTextField");
    if ($textField) {
    test::pass("Correctly found the hidden object");
    }
}
if object.exists(":Credit Card.Account Name:_javax.swing.JTextField")
    textField = findObject(
        ":Credit Card.Account Name:_javax.swing.JTextField")
    if textField
    Test.pass("Correctly found the hidden object")
    end
end
if {[object exists ":Credit Card.Account Name:_javax.swing.JTextField"]} {
    set textField [findObject \
        ":Credit Card.Account Name:_javax.swing.JTextField"]
    if {![isNull $textField]} {
    test pass "Correctly found the hidden object"
    }
}

使用这些技术,可以查询和访问AUT对象层次结构中的每个对象,而不管它们是否可见。

如何在Java对象上调用函数

Squish使您能够在任何Java对象上调用任何公共函数。(有关查找和查询对象的详细信息,请参阅如何查找和查询对象)。以下示例展示了如何创建一个Java对象:

s = java_lang_String("A string")
var s = new java_lang_String("A string");
my $s = java_lang_String->new("A string"); # "old"-style
my $s = new java_lang_String("A string");  # "new"-style
s = java_lang_String.new("A string")
set s [construct java_lang_String "A string"]

注意:在Squish脚本中引用由包名限定的Java对象时,正常点(".")被下划线("_")替换。这是因为在Squish支持的大多数脚本语言中,点不允许用作标识符名称,并且在某些情况下有特殊含义。另请参阅包装自定义类

以下示例使用计算器演示应用程序作为AUT。这个小型的JavaScript测试脚本将乘法按钮的文本从*更改为x

button = waitForObject(":frame0.*_javax.swing.JButton")
button.setText("x")
var button = waitForObject(":frame0.*_javax.swing.JButton");
button.setText("x");
my $button = waitForObject(":frame0.*_javax.swing.JButton");
$button->setText("x");
button = waitForObject(":frame0.*_javax.swing.JButton")
button.setText("x")
set button [waitForObject ":frame0.*_javax.swing.JButton"]
invoke $button setText "x"

还可以调用静态函数。以下是一个使用Java的静态函数 Integer.parseInt(String) 的例子。

i = java_lang_Integer.parseInt("12")
var i = java_lang_Integer.parseInt("12");
my $i = java_lang_Integer::parseInt("12");
i = java_lang_Integer.parseInt("12")
set i [invoke java_lang_Integer parseInt "12"]

传递一个 null

以下是如何创建Java中的 null,假设有一个Java方法调用,它需要一个 java.lang.Object 作为参数。

obj.doFoo(object.createNull(java_lang_Object))
obj.doFoo(object.createNull(java_lang_Object));
$obj->doFoo(object::createNull(java_lang_Object));
obj.doFoo(Object.createNull(LC::Java_lang_Object))
invoke $obj doFoo [object createNull java_lang_Object]

如何访问Java对象属性

Java对象可以有字段(有时也称为属性)。公共字段在Squish中是可访问的,如下例所示

point = java_awt_Point(5, 8)
test.log(point.x)
var point = new java_awt_Point(5, 8);
test.log(point.x);
my $point = java_awt_Point->new(5, 8); # "old"-style
my $point = new java_awt_Point(5, 8);  # "new"-style
test::log($point->x);
point = java_awt_Point.new(5, 8)
Test.log(point.x)
set point [construct java_awt_Point 5 8]
test log [toString [property get $point x]]

除了公共字段外,Squish还添加了从方法名派生出的合成属性,例如有 SomeType getSomething()boolean isSomething()void setSomething(SomeType someValue) 模式的 SomeType。有关详细信息,请参阅 如何使用API。在更改按钮文本的例子中使用 setText("x"),我们也可以用属性语法实现相同的效果。这里再次给出示例

button = waitForObject(":frame0.*_javax.swing.JButton")
button.text = "New Text"
test.log(button.text)
var button = waitForObject(":frame0.*_javax.swing.JButton");
button.text = "New Text";
test.log(button.text);
my $button = waitForObject(":frame0.*_javax.swing.JButton");
$button->text = "New Text";
test::log($button->text);
button = waitForObject(":frame0.*_javax.swing.JButton")
button.text = "New Text"
Test.log(button.text)
set button [waitForObject ":frame0.*_javax.swing.JButton"]
property set $button text "New Text"
test log [property get $button text]

当Squish遇到设置属性的代码时,它会自动执行适当的调用。例如,使用JavaScript,button.setText("x")。同样,如果我们尝试使用属性语法读取值,Squish将使用适当的getter语法,例如(再次使用JavaScript),var text = button.text 将被视为 var text = button.getText()

注意:Java合成属性,即Squish根据函数签名创建的属性,使得在测试脚本中添加更多验证点更容易。有关更多详细信息,请参阅 如何创建和使用验证点

如何使用Java便利API

本节介绍了Squish在Java API的基础上提供的脚本API,以便轻松执行常见用户操作,例如单击按钮、输入文本等。完整的API列表可在 Java便利API 部分中查看,该部分位于 工具参考手册 中。以下是几个示例,以说明API函数的使用方法。

clickButton(":frame0_Notepad$1")
type(":frame0_javax.swing.JTextArea", "Some text")
activateItem(":frame0_javax.swing.JMenuBar", "File")
activateItem(":frame0.File_javax.swing.JMenu", "Exit")
clickButton(":frame0_Notepad$1");
type(":frame0_javax.swing.JTextArea", "Some text");
activateItem(":frame0_javax.swing.JMenuBar", "File");
activateItem(":frame0.File_javax.swing.JMenu", "Exit");
clickButton(":frame0_Notepad$1");
type(":frame0_javax.swing.JTextArea", "Some text");
activateItem(":frame0_javax.swing.JMenuBar", "File");
activateItem(":frame0.File_javax.swing.JMenu", "Exit");
clickButton(":frame0_Notepad$1")
type(":frame0_javax.swing.JTextArea", "Some text")
activateItem(":frame0_javax.swing.JMenuBar", "File")
activateItem(":frame0.File_javax.swing.JMenu", "Exit")
invoke clickButton ":frame0_Notepad$1"
invoke type ":frame0_javax.swing.JTextArea" "Some text"
invoke activateItem ":frame0_javax.swing.JMenuBar" "File"
invoke activateItem ":frame0.File_javax.swing.JMenu" "Exit"

在这里,我们点击了一个按钮,输入了一些文本,并激活了一个菜单和菜单项。有关涵盖AWT/Swing和SWT,包括与许多不同控件(如行编辑、微调控件、列表、表格和树)交互的更多示例,请参阅 如何测试应用程序

完整的API包含比我们展示的三个API更多的函数。请注意,相同的API适用于AWT/Swing应用程序和SWT应用程序——唯一的区别是它们有不同的控件和对象名称。

如何创建和访问Java数组

访问Java数组

Java API中的一些方法返回 Array(在Squish中称为 JavaArray),而不是单个对象。此类数组的元素数量可通过 JavaArray.length 属性访问,并可以使用带有数组索引参数的 Object JavaArray.at(index) 方法访问单个元素。以下是一个列出JTabbedPane中JPanel的示例

tabPane = waitForObject(":Payment Form_javax.swing.JTabbedPane")
components = tabPane.getComponents()
for i in range(components.length):
    test.log("Component #%d: %s" % (i, components.at(i)))
var tabPane = waitForObject(":Payment Form_javax.swing.JTabbedPane");
var components = tabPane.getComponents();
for (var i = 0; i < components.length; ++i)
    test.log("Component #" + i + ": " + components.at(i));
my $tabPane = waitForObject(":Payment Form_javax.swing.JTabbedPane");
my $components = $tabPane->getComponents();
for (my $i = 0; $i < $components->length; ++$i) {
    test::log("Component #$i: ". $components->at($i) . "\n");
}
tabPane = waitForObject(":Payment Form_javax.swing.JTabbedPane")
components = tabPane.getComponents()
for i in 0...components.length
    Test.log("Component ##{i}: #{components.at(i)}")
end
set tabPane [waitForObject ":Payment Form_javax.swing.JTabbedPane"]
set components [invoke $tabPane getComponents]
for {set i 0} {$i < [property get $components length]} {incr i} {
    test log [concat "Component #$i: " \
        [toString [invoke $components at $i]]]
}

另一个示例在 如何测试树tst_tree 测试脚本中给出。

创建和使用JavaArrays

除了如图所示访问Squish函数返回的Java数组外,还可以创建自己的本地Java数组。下面是一些示例,以展示如何使用JavaArrays。请注意,如果我们以java.lang.Object的形式存储项,那么我们可以非常方便地存储我们喜欢的任何类型的数据。

    variousObjects = JavaArray(5) # creates a java.lang.Object[5] array
    variousObjects.set(0, waitForObject(names.payment_Form_Cancel_javax_swing_JButton))
    variousObjects.set(1, java_lang_Object())
    variousObjects.set(2, 4)           # converted to java.lang.Integer
    variousObjects.set(3, "some text") # converted to java.lang.String
    test.compare(variousObjects.at(0).text, "Cancel")
    test.compare(variousObjects.at(1).getClass().getName(), "java.lang.Object")
    test.compare(variousObjects.at(2), 4)
    test.compare(variousObjects.at(3), "some text")
    test.verify(variousObjects.length == 5)

    integers = JavaArray(42, "int") # creates an int[42] array
    integers.set(23, -71)
    test.compare(integers.at(23), -71)
    test.verify(integers.length == 42)

    strings = JavaArray(10, "java.lang.String") # java.lang.String[10]
    strings.set(4, "more text")
    test.compare(strings.at(4), "more text")
    test.verify(strings.length == 10)
    var variousObjects = new JavaArray(5); // creates a java.lang.Object[5] array
    variousObjects.set(0, waitForObject(names.paymentFormCancelJavaxSwingJButton));
    variousObjects.set(1, new java_lang_Object());
    variousObjects.set(2, 4);           // converted to java.lang.Integer
    variousObjects.set(3, "some text"); // converted to java.lang.String
    test.compare(variousObjects.at(0).text, "Cancel");
    test.compare(variousObjects.at(1).getClass().getName(), "java.lang.Object");
    test.compare(variousObjects.at(2), 4);
    test.compare(variousObjects.at(3), "some text");
    test.verify(variousObjects.length == 5);

    var integers = new JavaArray(42, "int"); // creates an int[42] array
    integers.set(23, -71);
    test.compare(integers.at(23), -71);
    test.verify(integers.length == 42);

    var strings = new JavaArray(10, "java.lang.String"); // java.lang.String[10]
    strings.set(4, "more text");
    test.compare(strings.at(4), "more text");
    test.verify(strings.length == 10);
    my $variousObjects = JavaArray->new(5); # creates a java.lang.Object[5] array
    $variousObjects->set(0, waitForObject($Names::payment_form_cancel_javax_swing_jbutton));
    $variousObjects->set(1, java_lang_Object->new());
    $variousObjects->set(2, 4);           # converted to java.lang.Integer
    $variousObjects->set(3, "some text"); # converted to java.lang.String
    #test::compare($variousObjects->at(0)->text, "Cancel");
    test::compare($variousObjects->at(1)->getClass()->getName(), "java.lang.Object");
    test::compare($variousObjects->at(2), 4);
    test::compare($variousObjects->at(3), "some text");
    test::verify($variousObjects->length == 5);

    my $integers = JavaArray->new(42, "int"); # creates an int[42] array
    $integers->set(23, -71);
    test::compare($integers->at(23), -71);
    test::verify($integers->length == 42);

    my $strings = JavaArray->new(10, "java.lang.String"); # java.lang.String[10]
    $strings->set(4, "more text");
    test::compare($strings->at(4), "more text");
    test::verify($strings->length == 10);
    variousObjects = JavaArray.new(5) # creates a java.lang.Object[5] array
    variousObjects.set(0, waitForObject(Names::Payment_Form_Cancel_javax_swing_JButton))
    variousObjects.set(1, LC::Java_lang_Object.new)
    variousObjects.set(2, 4)                                     # converted to java.lang.Integer
    variousObjects.set(3, "some text") # converted to java.lang.String
    Test.compare(variousObjects.at(0).text, "Cancel")
    Test.compare(variousObjects.at(1).getClass().getName(), "java.lang.Object")
    Test.compare(variousObjects.at(2), 4)
    Test.compare(variousObjects.at(3), "some text")
    Test.verify(variousObjects.length == 5)

    integers = JavaArray.new(42, "int") # creates an int[42] array
    integers.set(23, -71)
    Test.compare(integers.at(23), -71)
    Test.verify(integers.length == 42)

    strings = JavaArray.new(10, "java.lang.String") # java.lang.String[10]
    strings.set(4, "more text")
    Test.compare(strings.at(4), "more text")
    Test.verify(strings.length == 10)
    set variousObjects [construct JavaArray 5]
    invoke $variousObjects set 0 [waitForObject ":Payment Form.Cancel_javax.swing.JButton"]
    invoke $variousObjects set 1 [construct java_lang_Object]
    # converted to java.lang.Integer
    invoke $variousObjects set 2 4
    # converted to java.lang.String
    invoke $variousObjects set 3 "some text"
    test compare [property get [invoke $variousObjects at 0] text] "Cancel"
    test compare [invoke [invoke [invoke $variousObjects at 1] getClass] getName] "java.lang.Object"
    test compare [invoke $variousObjects at 2] 4
    test compare [invoke $variousObjects at 3] "some text"
    test compare [property get $variousObjects length] 5

    # creates an int[42] array
    set integers [construct JavaArray 42 "int"]
    invoke $integers set 23 [expr -71]
    test compare [invoke $integers at 23] -71
    test compare [property get $integers length] 42

    # java.lang.String[10]
    set strings [construct JavaArray 10 "java.lang.String"]
    invoke $strings set 4 "more text"
    test compare [invoke $strings at 4] "more text"
    test compare [property get $strings length] 10

JavaArray API在本地Java数组中进行了文档说明。

如何测试Java应用程序

在本节中,我们将了解Squish API如何使检查个别小部件的值和状态变得简单,以便我们可以测试应用程序的业务规则。

如教程中所述,我们可以使用Squish的录制功能来创建测试。然而,修改此类测试或从头开始使用代码创建测试通常很有用,尤其是当我们想测试涉及多个小部件的业务规则时。

通常,没有必要测试小部件的标准行为。例如,如果未选中的两值复选框在被点击后没有选中,那是在工具包中的bug,而不是我们代码中的bug。如果出现这种情况,我们可能需要编写一个解决方案(并为它编写测试),但通常我们不编写测试只是为了检查小部件是否按文档中的行为运行。另一方面,我们确实想测试的是我们的应用程序是否提供了我们打算构建入其中的业务规则。有些测试是与孤立的小部件相关的——例如,测试组合框是否包含适当的项。其他测试涉及小部件之间的依赖关系和交互。例如,如果我们有一组“支付方式”的单选按钮,我们将想要测试如果选择“现金”单选按钮,相关的支票和信用卡小部件都将被隐藏。

本节旨在解释并展示如何使用JavaScript、Perl、Python、Ruby和Tcl脚本语言访问各种Java小部件并使用这些小部件执行常见操作——例如获取和设置它们的属性。

完成本节后,您应该能够访问Java小部件,从这些Java小部件中收集数据,并对预期值执行测试。本章介绍的原则适用于所有Java小部件,因此即使您需要测试在此处未具体提到的小部件,您也应该没有任何问题。

无论我们是测试个别小部件还是小部件之间的依赖关系和交互,我们首先必须能够获取希望测试的小部件的引用。一旦我们有了引用,我们就可以验证它引用的小部件具有我们预期的值和状态。

要测试和验证组件及其属性或内容,首先需要在测试脚本中访问该组件。为了获取组件的引用,使用Object waitForObject(objectOrName)函数。此函数通过给定名称查找组件,并返回对其的引用。为此,我们需要知道要测试的组件名称,我们可以使用Spy工具(参看如何使用Spy)将对象添加到对象映射中(这样Squish可以记住它),然后将对象的名称(最好是它的符号名称)复制到剪贴板,以备粘贴到我们的测试中。如果我们需要收集许多组件的名称,可能在记录一个模拟测试期间这样做会更快和更容易,我们可以在手动编写的测试脚本中确保访问我们要验证的每个组件。这将导致Squish将所有相关名称添加到对象映射中,然后我们可以复制并粘贴到我们的代码中。另一种方法是按照上述方法获取容器组件的引用,然后使用Java的反射功能获取容器中组件的引用。我们将在本节中展示这两种方法。

如何测试Java AWT/Swing应用程序

在以下子节中,我们将关注测试Java AWT/Swing组件,包括单值组件(如按钮和微调框)和多值组件(如列表、表格和树)。我们还将介绍使用外部数据文件进行测试。如果您正在使用Java SWT,请跳到如何测试SWT应用程序

如何测试有状态和单值组件(Java—AWT/Swing)

在本节中,我们将了解如何测试examples/java/paymentform/PaymentForm.java示例程序。此程序使用许多基本的Java AWT/Swing组件,包括JButtonJCheckBoxJComboBoxJSpinnerJTextField。在我们对示例的覆盖率中,我们将展示如何检查单个组件的值和状态。我们还将演示如何测试表单的业务规则。

{}

"按信用卡支付"模式的PaymentForm示例。

当需要支付发票时,无论是销售点支付,还是信用卡通过电话支付时,都会调用PaymentForm。表单的Pay按钮只有在正确填写并填写有有效值时才应启用。我们必须测试的业务规则如下:

  • 在"现金"模式下,即当选中现金选项卡时:
    • 最低还款金额为一美元,最高为2000美元或应付款项,取较小者。
  • 在"支票"模式下,即当选中支票选项卡时:
    • 最低还款金额为10美元或应付款项的5%,取较大者,最高为250美元或应付款项,取较小者。
    • 支票日期不得早于30天前,不得晚于明天。
    • 银行名称、银行号码、账号和账号行编辑必须都不为空。
    • 已签署支票复选框必须被选中。
  • 在"卡"模式下,即当选中信用卡选项卡时:
    • 最低还款金额为10美元或应付款项的5%,取较大者,最高为5000美元或应付款项,取较小者。
    • 对于非维萨卡,发行日期不得早于三年前。
    • 到期日期必须比今天晚至少一个月。
    • 账号和账号行编辑必须不为空。

我们将编写三个测试,每个针对表单的每种模式。

支付表单的源代码位于目录中 SQUISHDIR/examples/java/paymentform,测试套件位于其下方的子目录中——例如,测试的Python版本位于目录 SQUISHDIR/examples/java/paymentform/suite_py,测试的JavaScript版本位于 SQUISHDIR/examples/java/paymentform/suite_js,依此类推。

我们首先将审查测试脚本,用于测试表单的“现金”模式。首先我们展示代码,然后解释它。

import os
def main():
    startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/java/paymentform/PaymentFormSwing.jar"')
    # Start with the correct tab
    tabWidgetName = ":Payment Form.Cash_com.froglogic.squish.awt.TabProxy"
    waitForObject(tabWidgetName)
    clickTab(tabWidgetName)

    # Business rule #1: the minimum payment is $1 and the maximum is
    # $2000 or the amount due whichever is smaller
    amountDueLabelName = ("{caption?='[$][0-9.,]*' "
        "type='javax.swing.JLabel' "
        "visible='true' window=':Payment Form_PaymentForm'}")
    amountDueLabel = waitForObject(amountDueLabelName)
    chars = []
    for char in str(amountDueLabel.getText()):
        if char.isdigit():
            chars.append(char)
    amount_due = cast("".join(chars), int)
    maximum = min(2000, amount_due)
    paymentSpinnerName = ("{type='javax.swing.JSpinner' visible='true' "
            "window=':Payment Form_PaymentForm'}")
    paymentSpinner = waitForObject(paymentSpinnerName)
    model = paymentSpinner.getModel()
    test.verify(model.minimum.intValue() == 1)
    test.verify(model.maximum.intValue() == maximum)

# Business rule #2: the Pay button is enabled (since the above tests
    # ensure that the payment amount is in range)
    payButtonName = ":Payment Form.Pay_javax.swing.JButton"
    payButton = waitForObject(payButtonName)
    test.verify(payButton.enabled)
function main()
{
    startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/java/paymentform/PaymentFormSwing.jar"');
    // Start with the correct tab
    var tabWidgetName = ":Payment Form.Cash_com.froglogic." +
        "squish.awt.TabProxy";
    waitForObject(tabWidgetName);
    clickTab(tabWidgetName);

    // Business rule #1: the minimum payment is $1 and the maximum is
    // $2000 or the amount due whichever is smaller
    var amountDueLabelName = "{caption?='[$][0-9.,]*' " +
        "type='javax.swing.JLabel' visible='true' " +
        "window=':Payment Form_PaymentForm'}";
    var amountDueLabel = waitForObject(amountDueLabelName);
    var amount_due = 0 + amountDueLabel.text.replace(/\D/g, "");
    var maximum = Math.min(2000, amount_due);

    var paymentSpinnerName = "{type='javax.swing.JSpinner' " +
        "visible='true' window=':Payment Form_PaymentForm'}";
    var paymentSpinner = waitForObject(paymentSpinnerName);
    var model = paymentSpinner.getModel();
    test.verify(model.minimum.intValue() == 1);
    test.verify(model.maximum.intValue() == maximum);

// Business rule #2: the Pay button is enabled (since the above tests
    // ensure that the payment amount is in range)
    var payButtonName = ":Payment Form.Pay_javax.swing.JButton";
    var payButton = waitForObject(payButtonName);
    test.verify(payButton.enabled);
}
sub main
{
    startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/java/paymentform/PaymentFormSwing.jar\"");
    # Start with the correct tab
    my $tabWidgetName = ":Payment Form.Cash_com.froglogic." .
        "squish.awt.TabProxy";
    waitForObject($tabWidgetName);
    clickTab($tabWidgetName);

    # Business rule #1: the minimum payment is $1 and the maximum is
    # $2000 or the amount due whichever is smaller
    my $amountDueLabelName = "{caption?='[\$][0-9.,]*' " .
        "type='javax.swing.JLabel' visible='true' " .
        "window=':Payment Form_PaymentForm'}";
    my $amountDueLabel = waitForObject($amountDueLabelName);
    my $amount_due = $amountDueLabel->text;
    $amount_due =~ s/\D//g; # remove non-digits
    my $maximum = 2000 < $amount_due ? 2000 : $amount_due;

    my $paymentSpinnerName = "{type='javax.swing.JSpinner' " .
        "visible='true' window=':Payment Form_PaymentForm'}";
    my $paymentSpinner = waitForObject($paymentSpinnerName);
    my $model = $paymentSpinner->getModel();
    test::verify($model->minimum->intValue() == 1);
    test::verify($model->maximum->intValue() == $maximum);

# Business rule #2: the Pay button is enabled (since the above tests
    # ensure that the payment amount is in range)
    my $payButtonName = ":Payment Form.Pay_javax.swing.JButton";
    my $payButton = waitForObject($payButtonName);
    test::verify($payButton->enabled);
}
# encoding: UTF-8
require 'squish'
include Squish

def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/java/paymentform/PaymentFormSwing.jar\"")
    # Start with the correct tab
    tabWidgetName = ":Payment Form.Cash_com.froglogic.squish.awt.TabProxy"
    waitForObject(tabWidgetName)
    clickTab(tabWidgetName)

    # Business rule #1: the minimum payment is $1 and the maximum is
    # $2000 or the amount due whichever is smaller
    amountDueLabelName = "{caption?='[$][0-9.,]*' " +
    "type='javax.swing.JLabel' " +
    "visible='true' window=':Payment Form_PaymentForm'}"
    amountDueLabel = waitForObject(amountDueLabelName)
    amount_due = String(amountDueLabel.text).gsub(/\D/, "").to_f
    maximum = 2000 < amount_due ? 2000 : amount_due
    paymentSpinnerName = "{type='javax.swing.JSpinner' visible='true' " +
    "window=':Payment Form_PaymentForm'}"
    paymentSpinner = waitForObject(paymentSpinnerName)
    model = paymentSpinner.getModel()
    Test.verify(model.minimum.intValue() == 1)
    Test.verify(model.maximum.intValue() == maximum)

    # Business rule #2: the Pay button is enabled (since the above tests
    # ensure that the payment amount is in range)
    payButtonName = ":Payment Form.Pay_javax.swing.JButton"
    payButton = waitForObject(payButtonName)
    Test.verify(payButton.enabled)
end
proc main {} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/java/paymentform/PaymentFormSwing.jar\""
    # Start with the correct tab
    set tabWidgetName ":Payment Form.Cash_com.froglogic.squish.awt.TabProxy"
    waitForObject $tabWidgetName
    invoke clickTab $tabWidgetName

    # Business rule #1: the minimum payment is $1 and the maximum is
    # $2000 or the amount due whichever is smaller
    set amountDueLabelName {{caption?='[$][0-9.,]*' \
        type='javax.swing.JLabel' visible='true' \
        window=':Payment Form_PaymentForm'}}
    set amountDueLabel [waitForObject $amountDueLabelName]
    set amountText [toString [property get $amountDueLabel text]]
    regsub -all {\D} $amountText "" amountText
    set amount_due [expr $amountText]
    set maximum [expr $amount_due < 2000 ? $amount_due : 2000]

    set paymentSpinnerName {{type='javax.swing.JSpinner' \
        visible='true' window=':Payment Form_PaymentForm'}}
    set paymentSpinner [waitForObject $paymentSpinnerName]
    set model [invoke $paymentSpinner getModel]
    set minimumAllowed [invoke [property get $model minimum] intValue]
    set maximumAllowed [invoke [property get $model maximum] intValue]
    test compare $minimumAllowed 1
    test compare $maximumAllowed $maximum

# Business rule #2: the Pay button is enabled (since the above tests
    # ensure that the payment amount is in range)
    set payButtonName ":Payment Form.Pay_javax.swing.JButton"
    waitForObject $payButtonName
    set payButton [findObject $payButtonName]
    test verify [property get $payButton enabled]
}

我们必须确保表单处于我们想要测试的模式。一般来说,我们获取可见小部件的方法始终相同:我们创建一个包含小部件名称的变量,然后调用Object waitForObject(objectOrName)函数来获取小部件的引用。一旦我们有引用,我们就可以用它来访问小部件的属性和调用小部件的方法。在这种情况下,我们不需要在选项卡名称上调用Object waitForObject(objectOrName)函数,因为我们不需要引用选项卡;相反,我们只需使用clickTab(objectOrName, tabText)函数来点击我们感兴趣的选项卡。我们是如何知道选项卡的名称的?我们运行了一个示例测试并点击了每个选项卡——结果Squish将这些选项卡名称放入了对象映射中,然后我们从那里复制它们。

要测试的第一个业务规则涉及允许的最小和最大支付金额。像往常一样,我们首先调用Object waitForObject(objectOrName)来获取我们需要访问的对象的引用——在这种情况下,从应付金额标签开始。由于应付标签的文本根据金额的不同而变化,我们不能给它一个固定的名称。因此,我们使用多属性(真实)名称并用通配符来识别它。通配符[$][0-9.,]*匹配以美元符号开头后跟零个或多个数字、点和小数的任何文本。Squish也可以进行正则表达式匹配——有关更多信息,请参阅Improving Object Identification

由于标签的文本可能包含货币符号和分组标记(例如,$1,700或€1.700),为了将其文本转换为整数,我们必须首先删除任何非数字字符。我们根据底层脚本语言以不同的方式完成此操作。(例如,在Python中,我们遍历每个字符,并将所有数字字符联合成单个字符串,并使用Object cast(object, type)函数,该函数接受一个对象以及期望将对象转换为的类型,并返回所需类型的对象——或失败时返回0。在JavaScript中,我们采用类似的方法,但对于Perl和Tcl,我们只是使用正则表达式删除非数字字符。)得到的整数是应付金额,因此我们现在可以简单地计算现金支付的最高金额。

已知最小和最大金额后,我们接着获取支付微调器的引用。(在这种情况下,我们没有从对象映射中获取名称,而是猜测了它。或者,我们可以使用自省,我们将在不久后使用的技术。)一旦我们有了微调器的引用,我们就检索它的数字模型。然后我们使用Boolean test.verify(condition)函数确保模型设置正确的最小和最大金额。(对于Tcl,我们使用了Boolean test.compare(value1, value2)函数而不是Boolean test.verify(condition),因为这更方便。)

在这种情况下,由于金额在范围内(因为它必须在我们刚刚检查后仍然在范围内),所以支付是被允许的,因此可以使用付费按钮应该被启用。《b translate="no">支付。再次强调,我们采取同样的方法进行测试:首先调用Object waitForObject(objectOrName)函数获取引用,然后进行测试——在这种情况下检查支付按钮是否启用。

尽管“现金”模式测试效果良好,但代码几乎相同的几个位置。因此,在创建“支票”和“卡片”模式的测试之前,我们将创建一些通用的函数,我们可以使用它们来重构测试。(用于创建共享代码的过程在下文中略有说明如何创建和使用共享数据和共享脚本——我们基本上需要做的是在测试套件的共享项脚本中创建一个新的脚本。)Python通用代码在common.py中,JavaScript通用代码在common.js中,依此类推。

    def clickTabbedPane(text):
        waitForObject(":Payment Form.%s_com.froglogic.squish.awt.TabProxy" % (
            text))
        clickTab(":Payment Form.%s_com.froglogic.squish.awt.TabProxy" % text)


def getAmountDue():
        amountDueLabel = waitForObject("{caption?='[$][0-9.,]*' "
                "type='javax.swing.JLabel' visible='true' "
                "window=':Payment Form_PaymentForm'}")
        chars = []
        for char in str(amountDueLabel.getText()):
            if char.isdigit():
                chars.append(char)
        return cast("".join(chars), int)


    def checkPaymentRange(minimum, maximum):
        paymentSpinner = waitForObject("{type='javax.swing.JSpinner' "
            "visible='true' window=':Payment Form_PaymentForm'}")
        model = paymentSpinner.getModel()
        test.verify(model.minimum.intValue() == minimum)
        test.verify(model.maximum.intValue() == maximum)
    function clickTabbedPane(text)
    {
        var tabbedPaneName = ":Payment Form." + text +
            "_com.froglogic.squish.awt.TabProxy";
        waitForObject(tabbedPaneName);
        clickTab(tabbedPaneName);
    }


function getAmountDue()
    {
        var amountDueLabel = waitForObject("{caption?='[$][0-9.,]*' " +
                "type='javax.swing.JLabel' visible='true' " +
                "window=':Payment Form_PaymentForm'}")
        return 0 + amountDueLabel.text.replace(/\D/g, "");
    }


    function checkPaymentRange(minimum, maximum)
    {
        var paymentSpinner = waitForObject("{type='javax.swing.JSpinner' " +
            "visible='true' window=':Payment Form_PaymentForm'}");
        var model = paymentSpinner.getModel();
        test.verify(model.minimum.intValue() == minimum);
        test.verify(model.maximum.intValue() == maximum);
    }
    sub clickTabbedPane
    {
        my $text = shift(@_);
        my $name = ":Payment Form.${text}_com.froglogic." .
         "squish.awt.TabProxy";
        waitForObject($name);
        clickTab($name);
    }


sub getAmountDue
    {
        my $amountDueLabel = waitForObject("{caption?='[\$][0-9.,]*' " .
                "type='javax.swing.JLabel' visible='true' " .
                "window=':Payment Form_PaymentForm'}");
        my $amount_due = $amountDueLabel->text;
        $amount_due =~ s/\D//g; # remove non-digits
        return $amount_due;
    }


    sub checkPaymentRange
    {
        my ($minimum, $maximum) = @_;
        my $paymentSpinner = waitForObject("{type='javax.swing.JSpinner' " .
            "visible='true' window=':Payment Form_PaymentForm'}");
        my $model = $paymentSpinner->getModel();
        test::verify($model->minimum->intValue() == $minimum);
        test::verify($model->maximum->intValue() == $maximum);
    }
    # encoding: UTF-8
    require 'squish'
    include Squish

    def clickTabbedPane(text)
        waitForObject(":Payment Form.#{text}_com.froglogic.squish.awt.TabProxy")
        clickTab(":Payment Form.#{text}_com.froglogic.squish.awt.TabProxy")
    end


def getAmountDue
        amountDueLabel = waitForObject("{caption?='[$][0-9.,]*' " +
                "type='javax.swing.JLabel' visible='true' " +
                "window=':Payment Form_PaymentForm'}")
        String(amountDueLabel.text).gsub(/\D/, "").to_f
    end

    def checkPaymentRange(minimum, maximum)
        paymentSpinner = waitForObject("{type='javax.swing.JSpinner' " +
            "visible='true' window=':Payment Form_PaymentForm'}")
        model = paymentSpinner.getModel()
        Test.verify(model.minimum.intValue() == minimum)
        Test.verify(model.maximum.intValue() == maximum)
    end

    def min(a, b)
        a < b ? a : b
    end

    def max(a, b)
        a < b ? b : a
    end
proc clickTabbedPane {text} {
    waitForObject ":Payment Form.${text}_com.froglogic.squish.awt.TabProxy"
    invoke clickTab ":Payment Form.${text}_com.froglogic.squish.awt.TabProxy"
}


proc getAmountDue {} {
    set amountDueLabel [waitForObject \
        {{caption?='[$][0-9.,]*' type='javax.swing.JLabel' \
            visible='true' window=':Payment Form_PaymentForm'}}]
    set amountText [toString [property get $amountDueLabel text]]
    regsub -all {\D} $amountText "" amountText
    return [expr $amountText]
}


proc checkPaymentRange {minimum maximum} {
    set paymentSpinner [waitForObject \
        {{type='javax.swing.JSpinner' visible='true' \
            window=':Payment Form_PaymentForm'}}]
    set model [invoke $paymentSpinner getModel]
    set minimumAllowed [invoke [property get $model minimum] intValue]
    set maximumAllowed [invoke [property get $model maximum] intValue]
    test compare $minimumAllowed $minimum
    test compare $maximumAllowed $maximum
}

现在我们可以为“支票”和“卡片”模式编写测试,并将更多的努力投入到测试业务规则而不仅仅是基本任务。我们已经将“支票”模式的代码分解成一个main函数——这对Squish来说很特殊,是Squish唯一会调用的函数——并且一些特定测试的支持函数,它们与上面显示的共享函数一起,使代码更容易管理。尽管main函数在test.py(或test.js等等)文件的末尾,但我们首先会展示它,然后展示特定测试的支持函数。

def main():
    startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/java/paymentform/PaymentFormSwing.jar"')
    # Import functionality needed by more than one test script
    source(findFile("scripts", "common.py"))

    # Start with the correct tab
    clickTabbedPane("Check")

    # Business rule #1: the minimum payment is $10 and the maximum is
    # $250 or the amount due whichever is smaller
    amount_due = getAmountDue()
    checkPaymentRange(10, min(250, amount_due))

    # Business rule #2: the check date must be no earlier than 30 days
    # ago and no later than tomorrow
    checkDateRange(-30, 1)

    # Business rule #3: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use waitForObjectExists()
    payButtonName = names.payment_Form_Pay_javax_swing_JButton
    payButton = waitForObjectExists(payButtonName)
    test.verify(not payButton.enabled)

    # Business rule #4: the check must be signed (and if it isn't we
    # will check the check box ready to test the next rule)
    ensureSignedCheckBoxIsChecked()

    # Business rule #5: the Pay button should be enabled since all the
    # previous tests pass, the check is signed and now we have filled in
    # the account details
    populateCheckFields()
    payButton = waitForObject(payButtonName)
    test.verify(payButton.enabled)
function main()
{
    startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/java/paymentform/PaymentFormSwing.jar"');
    // Import functionality needed by more than one test script
    source(findFile("scripts", "common.js"));

    // Start with the correct tab
    clickTabbedPane("Check");

    // Business rule #1: the minimum payment is $10 and the maximum is
    // $250 or the amount due whichever is smaller
    var amount_due = getAmountDue();
    checkPaymentRange(10, Math.min(250, amount_due));

    // Business rule #2: the check date must be no earlier than 30 days
    // ago and no later than tomorrow
    checkDateRange(-30, 1);

    // Business rule #3: the Pay button is disabled (since the form's data
    // isn't yet valid), so we use waitForObjectExists()
    var payButtonName = names.paymentFormPayJavaxSwingJButton;
    var payButton = waitForObjectExists(payButtonName);
    test.verify(!payButton.enabled);

    // Business rule #4: the check must be signed (and if it isn't we
    // will check the check box ready to test the next rule)
    ensureSignedCheckBoxIsChecked();

    // Business rule #5: the Pay button should be enabled since all the
    // previous tests pass, the check is signed and now we have filled in
    // the account details
    populateCheckFields();
    var payButton = waitForObject(payButtonName);
    test.verify(payButton.enabled);
}
sub main
{
    startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/java/paymentform/PaymentFormSwing.jar\"");
    # Import functionality needed by more than one test script
    source(findFile("scripts", "common.pl"));

    # Start with the correct tab
    clickTabbedPane("Check");

    # Business rule #1: the minimum payment is $10 and the maximum is
    # $250 or the amount due whichever is smaller
    my $amount_due = getAmountDue();
    checkPaymentRange(10, $amount_due < 250 ? $amount_due : 250);

    # Business rule #2: the check date must be no earlier than 30 days
    # ago and no later than tomorrow
    checkDateRange(-30, 1);

    # Business rule #3: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use waitForObjectExists()
    my $payButtonName = $Names::payment_form_pay_javax_swing_jbutton;
    my $payButton = waitForObjectExists($payButtonName);
    test::verify(!$payButton->enabled);

    # Business rule #4: the check must be signed (and if it isn't we
    # will check the check box ready to test the next rule)
    ensureSignedCheckBoxIsChecked;

    # Business rule #5: the Pay button should be enabled since all the
    # previous tests pass, the check is signed and now we have filled in
    # the account details
    populateCheckFields;
    $payButton = waitForObject($payButtonName);
    test::verify($payButton->enabled);
}
def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/java/paymentform/PaymentFormSwing.jar\"")
    # Import functionality needed by more than one test script
    require findFile("scripts", "common.rb")

    # Start with the correct tab
    clickTabbedPane("Check")

    # Business rule #1: the minimum payment is $10 and the maximum is
    # $250 or the amount due whichever is smaller
    amount_due = getAmountDue
    checkPaymentRange(10, min(250, amount_due))

    # Business rule #2: the check date must be no earlier than 30 days
    # ago and no later than tomorrow
    checkDateRange(-30, 1)

    # Business rule #3: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use waitForObjectExists()
    payButtonName = Names::Payment_Form_Pay_javax_swing_JButton
    payButton = waitForObjectExists(payButtonName)
    Test.verify(!payButton.enabled)

    # Business rule #4: the check must be signed (and if it isn't we
    # will check the check box ready to test the next rule)
    ensureSignedCheckBoxIsChecked

    # Business rule #5: the Pay button should be enabled since all the
    # previous tests pass, the check is signed and now we have filled in
    # the account details
    populateCheckFields
    payButton = waitForObject(payButtonName)
    Test.verify(payButton.enabled)
end
proc main {} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/java/paymentform/PaymentFormSwing.jar\""
    # Import functionality needed by more than one test script
    source [findFile "scripts" "common.tcl"]

    # Start with the correct tab
    clickTabbedPane "Check"

    # Business rule #1: the minimum payment is $10 and the maximum is
    # $250 or the amount due whichever is smaller
    set amount_due [getAmountDue]
    set maximum [expr 250 > $amount_due ? $amount_due : 250]
    checkPaymentRange 10 $maximum

    # Business rule #2: the check date must be no earlier than 30 days
    # ago and no later than tomorrow
    checkDateRange -30 1

    # Business rule #3: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use waitForObjectExists()
    set payButtonName ":Payment Form.Pay_javax.swing.JButton"
    set payButton [waitForObjectExists $payButtonName]
    test verify [expr ![property get $payButton enabled]]

    # Business rule #4: the check must be signed (and if it isn't we
    # will check the check box ready to test the next rule)
    ensureSignedCheckBoxIsChecked

    # Business rule #5: the Pay button should be enabled since all the
    # previous tests pass, the check is signed and now we have filled in
    # the account details
    populateCheckFields
    set payButton [waitForObject $payButtonName]
    test verify [property get $payButton enabled]
}

source(filename)函数(或Ruby中的require),用于读取脚本并执行它。通常这种脚本仅用于明确定义事物——例如,函数——然后这些函数成为测试脚本可用。

第一个业务规则与之前非常相似,但由于共享的checkPaymentRange函数,代码要短得多。对于第二个规则的测试甚至更简单,因为我们已经把所有代码放在一个单独的checkDateRange函数中,我们将在下一刻查看这个函数。

第三条规则检查Pay按钮被禁用,因为在当前阶段表单数据无效。(例如,支票未签署,没有填写账目详细信息。)第四条规则用于证实检查已被签署——这是我们在特定的ensureSignedCheckBoxIsChecked函数中明确使其发生的事情。

对于第五条规则,我们将假数据填充到账户行编辑中。最后,所有的小部件都有了有效的值,所以Pay按钮应该被启用,最后一行测试了这一点。

这里是有用的函数,每个函数后面都有一段非常简短的说明。

def checkDateRange(daysBeforeToday, daysAfterToday):
    calendar = java_util_Calendar.getInstance()
    calendar.add(java_util_Calendar.DAY_OF_MONTH, daysBeforeToday)
    earliest = calendar.getTime()
    calendar.setTime(java_util_Date())
    calendar.add(java_util_Calendar.DAY_OF_MONTH, daysAfterToday)
    latest = calendar.getTime()
    checkDateSpinner = waitForObject(names.check_JPanel)
    model = checkDateSpinner.getModel()
    AnyClass = calendar.getClass()
    SimpleDateFormatClass = AnyClass.forName("java.text.SimpleDateFormat")
    formatter = SimpleDateFormatClass.newInstance()
    formatter.applyPattern("yyyy-MM-dd")
    test.verify(formatter.format(model.getStart()) ==
                formatter.format(earliest))
    test.verify(formatter.format(model.getEnd()) ==
function checkDateRange(daysBeforeToday, daysAfterToday)
{
    var checkDateSpinner = waitForObject({"container": names.paymentFormCheckComFroglogicSquishAwtTabProxy, "type":'javax.swing.JSpinner', "visible":'true'});
    var model = checkDateSpinner.getModel();
    var calendar = java_util_Calendar.getInstance();
    calendar.add(java_util_Calendar.DAY_OF_MONTH, daysBeforeToday);
    var earliest = calendar.getTime();
    calendar.setTime(new java_util_Date());
    calendar.add(java_util_Calendar.DAY_OF_MONTH, daysAfterToday);
    var latest = calendar.getTime();
    var AnyClass = calendar.getClass();
    var SimpleDateFormatClass = AnyClass.forName(
        "java.text.SimpleDateFormat");
    var formatter = SimpleDateFormatClass.newInstance();
    formatter.applyPattern("yyyy-MM-dd");
    test.verify(formatter.format(model.getStart()) ==
                formatter.format(earliest));
    test.verify(formatter.format(model.getEnd()) ==
                formatter.format(latest));
}
sub checkDateRange
{
    my ($daysBeforeToday, $daysAfterToday) = @_;
    my $calendar = java_util_Calendar::getInstance();
    $calendar->add(java_util_Calendar->DAY_OF_MONTH, $daysBeforeToday);
    my $earliest = $calendar->getTime();
    $calendar->setTime(java_util_Date->new());
    $calendar->add(java_util_Calendar->DAY_OF_MONTH, $daysAfterToday);
    my $latest = $calendar->getTime();
    my $checkDateSpinner = waitForObject($Names::check_check_date_jspinner);
    my $model = $checkDateSpinner->getModel();
    my $simpleDateFormatClass = java_lang_Class::forName("java.text.SimpleDateFormat");
    my $formatter = $simpleDateFormatClass->newInstance();
    $formatter->applyPattern("yyyy-MM-dd");
    test::verify($formatter->format($model->getStart()) eq
                 $formatter->format($earliest));
    test::verify($formatter->format($model->getEnd()) eq
                 $formatter->format($latest));
}
def checkDateRange(daysBeforeToday, daysAfterToday)
    calendar = LC::Java_util_Calendar.getInstance()
    calendar.add(LC::Java_util_Calendar.DAY_OF_MONTH, daysBeforeToday)
    earliest = calendar.getTime()
    calendar.setTime(LC::Java_util_Date.new)
    calendar.add(LC::Java_util_Calendar.DAY_OF_MONTH, daysAfterToday)
    latest = calendar.getTime()
    checkDateSpinner = waitForObject({:basetype => "javax.swing.JPanel", :container => Names::Payment_Form_Check_com_froglogic_squish_awt_TabProxy, :occurrence => 1, :visible => true})
    model = checkDateSpinner.getModel()
    simpleDateFormatClass = LC::Java_lang_Class.forName("java.text.SimpleDateFormat")
    formatter = simpleDateFormatClass.newInstance()
    formatter.applyPattern("yyyy-MM-dd")
    Test.verify(formatter.format(model.getStart()) ==
    formatter.format(earliest))
    Test.verify(formatter.format(model.getEnd()) ==
    formatter.format(latest))
end
proc checkDateRange {daysBeforeToday daysAfterToday} {
    set checkDateSpinner [waitForObject \
        {{container=':Payment Form.Check_com.froglogic.squish.awt.TabProxy' \
            type='javax.swing.JSpinner' visible='true'}}]
    set AnyClass [invoke $checkDateSpinner getClass]
    set SimpleDateFormatClass [invoke $AnyClass forName \
        "java.text.SimpleDateFormat"]
    set formatter [invoke $SimpleDateFormatClass newInstance]
    invoke $formatter applyPattern "yyyy-MM-dd"
    set model [invoke $checkDateSpinner getModel]
    set minimumAllowed [invoke $formatter format [invoke $model getStart]]
    set maximumAllowed [invoke $formatter format [invoke $model getEnd]]
    set calendar [invoke java_util_Calendar getInstance]
    invoke $calendar add [property get java_util_Calendar DAY_OF_MONTH] \
        $daysBeforeToday
    set minimumDate [invoke $formatter format [invoke $calendar getTime]]
    invoke $calendar setTime [construct java_util_Date]
    invoke $calendar add [property get java_util_Calendar DAY_OF_MONTH] \
        $daysAfterToday
    set maximumDate [invoke $formatter format [invoke $calendar getTime]]
    test compare $minimumAllowed $minimumDate
    test compare $maximumAllowed $maximumDate
}

checkDateRange函数中,我们测试一个JSpinnerSpinnerDateModel属性。请注意,我们使用统一格式将日期作为字符串进行比较。

虽然Squish可以自动提供大部分Java API的访问权限,但在某些情况下,我们需要访问默认情况下不可用的类。在这个函数中,我们需要几个不可用的类:java.util.Calendarjava.text.SimpleDateFormat。实际上这并不是一个问题,因为我们总能通过在squishserver中注册更多的类(无论是标准类还是自定义类)来解决这个问题——有关详情,包括如何在无需注册此类或使用.ini文件的情况下访问这些类,请参阅包装自定义类。在这种情况下,我们在java.ini中添加了几个额外的类,这里只有两行:

[general]
AutClasses="java.util.Calendar","java.util.Date",\
"java.text.DateFormat","java.text.SimpleDateFormat"

实际上,我们只为Perl版本使用了这种方法;对其他版本,我们利用Java的反向工程功能创建了java.text DateFormatjava.text.SimpleDateFormat类的实例。

def ensureSignedCheckBoxIsChecked():
    checkSignedCheckBox = waitForObject(
            names.check_Check_Signed_javax_swing_JCheckBox)
    if not checkSignedCheckBox.isSelected():
        clickButton(checkSignedCheckBox)
    test.verify(checkSignedCheckBox.isSelected())
function ensureSignedCheckBoxIsChecked()
{
    var checkSignedCheckBox = waitForObject(
            names.checkCheckSignedJavaxSwingJCheckBox);
    if (!checkSignedCheckBox.isSelected()) {
        clickButton(checkSignedCheckBox);
        }
    test.verify(checkSignedCheckBox.isSelected());
}
sub ensureSignedCheckBoxIsChecked
{
    my $checkSignedCheckBox = waitForObject(
            $Names::check_check_signed_javax_swing_jcheckbox);
    if (!$checkSignedCheckBox->isSelected()) {
        clickButton($checkSignedCheckBox);
    }
    test::verify($checkSignedCheckBox->isSelected());
}
def ensureSignedCheckBoxIsChecked
    checkSignedCheckBox = waitForObject(
    Names::Check_Check_Signed_javax_swing_JCheckBox)
    if !checkSignedCheckBox.isSelected()
        clickButton(checkSignedCheckBox)
    end
    Test.verify(checkSignedCheckBox.isSelected())
end
proc ensureSignedCheckBoxIsChecked {} {
    set checkSignedCheckBox [waitForObject \
        ":Check.Check Signed_javax.swing.JCheckBox"]
    if {![invoke $checkSignedCheckBox isSelected]} {
        invoke clickButton $checkSignedCheckBox
    }
    test verify [invoke $checkSignedCheckBox isSelected]
}

ensureSignedCheckBoxIsChecked函数检查一个JCheckBox的状态,如果它没有被选中,就通过点击来选中它。然后函数验证复选框确实被选中。

def populateCheckFields():
    bankNameLineEdit = waitForObject(
        names.check_Bank_Name_javax_swing_JTextField)
    type(bankNameLineEdit, "A Bank")
    bankNumberLineEdit = waitForObject(
        names.check_Bank_Number_javax_swing_JTextField)
    type(bankNumberLineEdit, "88-91-33X")
    accountNameLineEdit = waitForObject(
        names.check_Account_Name_javax_swing_JTextField)
    type(accountNameLineEdit, "An Account")
    accountNumberLineEdit = waitForObject(
        names.check_Account_Number_javax_swing_JTextField)
    type(accountNumberLineEdit, "932745395")
function populateCheckFields()
{
    var bankNameLineEdit = waitForObject(
        names.checkBankNameJavaxSwingJTextField);
    type(bankNameLineEdit, "A Bank");
    var bankNumberLineEdit = waitForObject(
        names.checkBankNumberJavaxSwingJTextField);
    type(bankNumberLineEdit, "88-91-33X");
    var accountNameLineEdit = waitForObject(
        names.checkAccountNameJavaxSwingJTextField);
    type(accountNameLineEdit, "An Account");
    var accountNumberLineEdit = waitForObject(
        names.checkAccountNumberJavaxSwingJTextField);
    type(accountNumberLineEdit, "932745395");
}
sub populateCheckFields
{
    my $bankNameLineEdit = waitForObject(
        $Names::check_bank_name_javax_swing_jtextfield);
    type($bankNameLineEdit, "A Bank");
    my $bankNumberLineEdit = waitForObject(
        $Names::check_bank_number_javax_swing_jtextfield);
    type($bankNumberLineEdit, "88-91-33X");
    my $accountNameLineEdit = waitForObject(
        $Names::check_account_name_javax_swing_jtextfield);
    type($accountNameLineEdit, "An Account");
    my $accountNumberLineEdit = waitForObject(
        $Names::check_account_number_javax_swing_jtextfield);
    type($accountNumberLineEdit, "932745395");
}
def populateCheckFields
    bankNameLineEdit = waitForObject(
    Names::Check_Bank_Name_javax_swing_JTextField)
    type(bankNameLineEdit, "A Bank")
    bankNumberLineEdit = waitForObject(
    Names::Check_Bank_Number_javax_swing_JTextField)
    type(bankNumberLineEdit, "88-91-33X")
    accountNameLineEdit = waitForObject(
    Names::Check_Account_Name_javax_swing_JTextField)
    type(accountNameLineEdit, "An Account")
    accountNumberLineEdit = waitForObject(
    Names::Check_Account_Number_javax_swing_JTextField)
    type(accountNumberLineEdit, "932745395")
end
proc populateCheckFields {} {
    set bankNameLineEdit [waitForObject \
        ":Check.Bank Name:_javax.swing.JTextField"]
    invoke type $bankNameLineEdit "A Bank"
    set bankNumberLineEdit [waitForObject \
        ":Check.Bank Number:_javax.swing.JTextField"]
    invoke type $bankNumberLineEdit "88-91-33X"
    set accountNameLineEdit [waitForObject \
        ":Check.Account Name:_javax.swing.JTextField"]
    invoke type $accountNameLineEdit "An Account"
    set accountNumberLineEdit [waitForObject \
        ":Check.Account Number:_javax.swing.JTextField"]
    invoke type $accountNumberLineEdit "932745395"
}

populateCheckFields函数填写了一些JTextField——这,连同设置日期和之前的复选框检查——应该确保Pay按钮处于可用状态,这一点在调用populateCheckFields函数后在main函数中被检查。

请注意,我们使用了type(objectOrName, text)函数来模拟用户输入文本。通常直接设置小部件属性不如模拟用户交互——毕竟,我们通常需要测试的是用户体验到的应用程序的行为。

现在我们已经准备好查看表单业务逻辑的最后一个测试——“卡片”模式的测试。由于其测试规范需要测试更多的事项,所以代码更长一点,但所有的原理都与我们已经看到的测试相同。

def main():
    startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/java/paymentform/PaymentFormSwing.jar"')
    source(findFile("scripts", "common.py"))

    # Start with the correct tab
    clickTabbedPane("Credit Card")

    # Business rule #1: the minimum payment is $10 or 5% of the amount due
    # whichever is larger and the maximum is $5000 or the amount due
    # whichever is smaller
    amount_due = getAmountDue()
    checkPaymentRange(max(10, amount_due / 20.0), min(5000, amount_due))

    # Business rule #2: for non-Visa cards the issue date must be no
    # earlier than 3 years ago
    # Business rule #3: the expiry date must be at least a month later
    # than today---we make sure that this is the case for the later tests
    checkCardDateEdits()

    # Business rule #4: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use waitForObjectExists()
    payButtonName = names.payment_Form_Pay_javax_swing_JButton
    payButton = waitForObjectExists(payButtonName)
    test.verify(not payButton.enabled)

    # Business rule #5: the Pay button should be enabled since all the
    # previous tests pass, and now we have filled in the account details
    populateCardFields()
    payButton = waitForObject(payButtonName)
    test.verify(payButton.enabled)
function main()
{
    startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/java/paymentform/PaymentFormSwing.jar"');
    source(findFile("scripts", "common.js"));

    // Start with the correct tab
    clickTabbedPane("Credit Card");

    // Business rule #1: the minimum payment is $10 or 5% of the amount
    // due whichever is larger and the maximum is $5000 or the amount
    // due whichever is smaller
    var amount_due = getAmountDue();
    checkPaymentRange(Math.max(10, amount_due / 20.0),
                      Math.min(5000, amount_due));

    // Business rule #2: for non-Visa cards the issue date must be no
    // earlier than 3 years ago
    // Business rule #3: the expiry date must be at least a month later
    // than today---we make sure that this is the case for the later tests
    checkCardDateEdits();

    // Business rule #4: the Pay button is disabled (since the form's data
    // isn't yet valid), so we use waitForObjectExists()
    var payButtonName = names.paymentFormPayJavaxSwingJButton;
    var payButton = waitForObjectExists(payButtonName);
    test.verify(!payButton.enabled);

    // Business rule #5: the Pay button should be enabled since all the
    // previous tests pass, and now we have filled in the account details
    populateCardFields();
    var payButton = waitForObject(payButtonName);
    test.verify(payButton.enabled);
}
sub main
{
    startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/java/paymentform/PaymentFormSwing.jar\"");
    source(findFile("scripts", "common.pl"));

    # Start with the correct tab
    clickTabbedPane("Credit Card");

    # Business rule #1: the minimum payment is $10 or 5% of the amount
    # due whichever is larger and the maximum is $5000 or the amount
    # due whichever is smaller
    my $amount_due = getAmountDue();
    my $minimum = $amount_due / 20.0 > 10 ? $amount_due / 20.0 : 10;
    my $maximum = $amount_due < 5000 ? $amount_due : 5000;
    checkPaymentRange($minimum, $maximum);

    # Business rule #2: for non-Visa cards the issue date must be no
    # earlier than 3 years ago
    # Business rule #3: the expiry date must be at least a month later
    # than today---we make sure that this is the case for the later tests
    checkCardDateEdits;

    # Business rule #4: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use waitForObjectExists()
    my $payButtonName = $Names::payment_form_pay_javax_swing_jbutton;
    my $payButton = waitForObjectExists($payButtonName);
    test::verify(!$payButton->enabled);

    # Business rule #5: the Pay button should be enabled since all the
    # previous tests pass, and now we have filled in the account details
    populateCardFields;
    $payButton = waitForObject($payButtonName);
    test::verify($payButton->enabled);
}
def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/java/paymentform/PaymentFormSwing.jar\"")
    require findFile("scripts", "common.rb")

    # Start with the correct tab
    clickTabbedPane("Credit Card")

    # Business rule #1: the minimum payment is $10 or 5% of the amount due
    # whichever is larger and the maximum is $5000 or the amount due
    # whichever is smaller
    amount_due = getAmountDue
    checkPaymentRange(max(10, amount_due / 20.0), min(5000, amount_due))

    # Business rule #2: for non-Visa cards the issue date must be no
    # earlier than 3 years ago
    # Business rule #3: the expiry date must be at least a month later
    # than today---we make sure that this is the case for the later tests
    checkCardDateEdits

    # Business rule #4: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use waitForObjectExists()
    payButtonName = Names::Payment_Form_Pay_javax_swing_JButton
    payButton = waitForObjectExists(payButtonName)
    Test.verify(!payButton.enabled)

    # Business rule #5: the Pay button should be enabled since all the
    # previous tests pass, and now we have filled in the account details
    populateCardFields
    payButton = waitForObject(payButtonName)
    Test.verify(payButton.enabled)
end
proc main {} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/java/paymentform/PaymentFormSwing.jar\""
    # Import functionality needed by more than one test script
    source [findFile "scripts" "common.tcl"]

    # Start with the correct tab
    clickTabbedPane "Credit Card"

    # Business rule #1: the minimum payment is $10 or 5% of the amount due
    # whichever is larger and the maximum is $5000 or the amount due
    # whichever is smaller
    set amount_due [getAmountDue]
    set five_percent [expr $amount_due / 20]
    set minimum [expr 10 > $five_percent ? 10 : $five_percent]
    set maximum [expr 5000 > $amount_due ? $amount_due : 5000]
    checkPaymentRange $minimum $maximum

    # Business rule #2: for non-Visa cards the issue date must be no
    # earlier than 3 years ago
    # Business rule #3: the expiry date must be at least a month later
    # than today---we make sure that this is the case for the later tests
    checkCardDateEdits

    # Business rule #4: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use waitForObjectExists()
    set payButtonName ":Payment Form.Pay_javax.swing.JButton"
    set payButton [waitForObjectExists $payButtonName]
    test verify [expr ![property get $payButton enabled]]

    # Business rule #5: the Pay button should be enabled since all the
    # previous tests pass, and now we have filled in the account details
    populateCardFields
    set payButton [waitForObject $payButtonName]
    test verify [property get $payButton enabled]
}

就像我们对“检查”模式的main函数所做的那样,我们将几乎每个测试都封装在一个单独的函数中。这使得main函数更加简洁和清晰,也使得独立开发和测试每个测试更加容易。

以下是支持函数,每个函数都附带一些简短的讨论内容。

def checkCardDateEdits():
    # (1) set the card type to any non-Visa card
    cardTypeComboBox = waitForObject(
        names.credit_Card_Card_Type_javax_swing_JComboBox)
    for index in range(cardTypeComboBox.getItemCount()):
        if cardTypeComboBox.getItemAt(index).toString() != "Visa":
            cardTypeComboBox.setSelectedIndex(index)
            break
    # (2) find the two date spinners
    creditCardTabPane = waitForObject(names.payment_Form_Credit_Card_com_froglogic_squish_awt_TabProxy).component
    spinners = []
    for i in range(creditCardTabPane.getComponentCount()):
        component = creditCardTabPane.getComponent(i)
        if (component.getClass().toString() ==
            "class javax.swing.JSpinner"):
            spinners.append(component)
    test.verify(len(spinners) == 2)
    # (3) check the issue date spinner's minimum date
    calendar = java_util_Calendar.getInstance()
    calendar.add(java_util_Calendar.YEAR, -3)
    date = calendar.getTime()
    issueDateSpinner = spinners[0]
    model = issueDateSpinner.getModel()
    java_lang_Class.forName("java.text.SimpleDateFormat")
    formatter = java_text_SimpleDateFormat()
    formatter.applyPattern("yyyy-MM-dd")
    test.verify(formatter.format(model.getStart()) ==
                formatter.format(date))
    # (4) set the expiry date more than a month later
    # than now for later tests
    calendar.setTime(java_util_Date())
    calendar.add(java_util_Calendar.DAY_OF_MONTH, 35)
    expiryDateSpinner = spinners[1]
    expiryDateSpinner.setValue(calendar.getTime())
function checkCardDateEdits()
{
    // (1) set the card type to any non-Visa card
    var cardTypeComboBox = waitForObject(
        names.creditCardCardTypeJavaxSwingJComboBox);
    for (var index = 0; index < cardTypeComboBox.getItemCount(); ++index) {
        if (cardTypeComboBox.getItemAt(index).toString() != "Visa") {
            cardTypeComboBox.setSelectedIndex(index);
            break;
        }
    }
    // (2) find the two date spinners
    var creditCardTabPane = waitForObject(names.paymentFormCreditCardComFroglogicSquishAwtTabProxy).component;
    var spinners = [];
    for (var i = 0; i < creditCardTabPane.getComponentCount(); ++i) {
        var component = creditCardTabPane.getComponent(i);
        if (component.getClass().toString() ==
            "class javax.swing.JSpinner") {
            spinners.push(component);
        }
    }
    test.verify(spinners.length == 2);
    // (3) check the issue date spinner's minimum date
    var calendar = java_util_Calendar.getInstance();
    calendar.add(java_util_Calendar.YEAR, -3);
    var threeYearsAgo = calendar.getTime();
    var issueDateSpinner = spinners[0];
    var model = issueDateSpinner.getModel();
    java_lang_Class.forName("java.text.SimpleDateFormat");
    var formatter = new java_text_SimpleDateFormat();
    formatter.applyPattern("yyyy-MM-dd");
    test.verify(formatter.format(model.getStart()) ==
                formatter.format(threeYearsAgo));
    // (4) set the expiry date more than a month later
    // than now for later tests
    calendar.setTime(new java_util_Date());
    calendar.add(java_util_Calendar.DAY_OF_MONTH, 35);
    var expiryDateSpinner = spinners[1];
    expiryDateSpinner.setValue(calendar.getTime());
}
sub checkCardDateEdits
{
    # (1) set the card type to any non-Visa card
    my $cardTypeComboBox = waitForObject(
        $Names::credit_card_card_type_javax_swing_jcombobox);
    for (my $index = 0; $index < $cardTypeComboBox->getItemCount();
        ++$index) {
        if ($cardTypeComboBox->getItemAt($index)->toString() != "Visa") {
            $cardTypeComboBox->setSelectedIndex($index);
            last;
        }
    }
    # (2) find the two date spinners
    my $creditCardTabPane = waitForObject($Names::payment_form_credit_card_com_froglogic_squish_awt_tabproxy)->component;
    my @spinners = ();
    for (my $i = 0; $i < $creditCardTabPane->getComponentCount(); ++$i) {
        my $component = $creditCardTabPane->getComponent($i);
        if ($component->getClass()->toString() eq
            "class javax.swing.JSpinner") {
            push @spinners, $component;
        }
    }
    test::verify(@spinners == 2);
    # (3) check the issue date spinner's minimum date
    my $calendar = java_util_Calendar::getInstance();
    $calendar->add(java_util_Calendar->YEAR, -3);
    my $date = $calendar->getTime();
    my $issueDateSpinner = $spinners[0];
    my $model = $issueDateSpinner->getModel();
    java_lang_Class::forName("java.text.SimpleDateFormat");
    my $formatter = new java_text_SimpleDateFormat();
    $formatter->applyPattern("yyyy-MM-dd");
    test::verify($formatter->format($model->getStart()) eq
                 $formatter->format($date));
    # (4) set the expiry date more than a month later than
    # now for later tests
    $calendar->setTime(java_util_Date->new());
    $calendar->add(java_util_Calendar->DAY_OF_MONTH, 35);
    my $expiryDateSpinner = $spinners[1];
    $expiryDateSpinner->setValue($calendar->getTime());
}
def checkCardDateEdits
    # (1) set the card type to any non-Visa card
    cardTypeComboBox = waitForObject(
    Names::Credit_Card_Card_Type_javax_swing_JComboBox)
    for index in 0...cardTypeComboBox.getItemCount()
        if cardTypeComboBox.getItemAt(index).toString() != "Visa"
            cardTypeComboBox.setSelectedIndex(index)
            break
        end
    end
    # (2) find the two date spinners
    creditCardTabPane = waitForObject({:caption => "Credit Card", :type => "com.froglogic.squish.awt.TabProxy", :window => Names::Payment_Form_PaymentForm}).component
    spinners = []
    for i in 0...creditCardTabPane.getComponentCount()
        component = creditCardTabPane.getComponent(i)
        if (component.getClass().toString() ==
        "class javax.swing.JSpinner")
            spinners << component
        end
    end
    Test.verify(spinners.length == 2)
    # (3) check the issue date spinner's minimum date
    calendar = LC::Java_util_Calendar.getInstance()
    calendar.add(LC::Java_util_Calendar.YEAR, -3)
    date = calendar.getTime()
    issueDateSpinner = spinners[0]
    model = issueDateSpinner.getModel()
    LC::Java_lang_Class.forName("java.text.SimpleDateFormat")
    formatter = LC::Java_text_SimpleDateFormat.new
    formatter.applyPattern("yyyy-MM-dd")
    Test.verify(formatter.format(model.getStart()) ==
    formatter.format(date))
    # (4) set the expiry date more than a month later
    # than now for later tests
    calendar.setTime(LC::Java_util_Date.new())
    calendar.add(LC::Java_util_Calendar.DAY_OF_MONTH, 35)
    expiryDateSpinner = spinners[1]
    expiryDateSpinner.setValue(calendar.getTime())
end
proc checkCardDateEdits {} {
    # (1) set the card type to any non-Visa card
    set cardTypeComboBox [waitForObject \
        ":Credit Card.Card Type:_javax.swing.JComboBox"]
    set count [invoke $cardTypeComboBox getItemCount]
    for {set index 0} {$index < $count} {incr index} {
        if {[invoke $cardTypeComboBox getItemAt $index] != "Visa"} {
            invoke $cardTypeComboBox setSelectedIndex $index
            break
        }
    }
    # (2) find the two date spinners
    set creditCardTabPaneProxy [waitForObject \
        ":Payment Form.Credit Card_com.froglogic.squish.awt.TabProxy"]
    set creditCardTabPane [property get $creditCardTabPaneProxy component]
    set spinners {}
    set count [invoke $creditCardTabPane getComponentCount]
    for {set index 0} {$index < $count} {incr index} {
        set component [invoke $creditCardTabPane getComponent $index]
        set classname [invoke [invoke $component getClass] toString]
        if {$classname == "class javax.swing.JSpinner"} {
            lappend spinners $component
        }
    }
    test compare [llength $spinners] 2
    # (3) check the issue date spinner's minimum date
    set calendar [invoke java_util_Calendar getInstance]
    invoke $calendar add [property get java_util_Calendar YEAR] -3
    set issueDateSpinner [lindex $spinners 0]
    set model [invoke $issueDateSpinner getModel]
    invoke java_lang_Class forName "java.text.SimpleDateFormat"
    set formatter [construct java_text_SimpleDateFormat]
    invoke $formatter applyPattern "yyyy-MM-dd"
    set minimumAllowed [invoke $formatter format [invoke $model getStart]]
    set minimumDate [invoke $formatter format [invoke $calendar getTime]]
    test compare $minimumAllowed $minimumDate
    # (4) set the expiry date more than a month later than now for
    # later tests
    invoke $calendar setTime [construct java_util_Date]
    invoke $calendar add [property get java_util_Calendar DAY_OF_MONTH] 35
    set expiryDateSpinner [lindex $spinners 1]
    invoke $expiryDateSpinner setValue [invoke $calendar getTime]
}

第二和第三业务规则由特定于测试的checkCardDateEdits函数处理。对于第二个业务规则,我们需要卡类型下拉列表处于除Visa以外的任何卡类型,因此我们将迭代下拉列表的项,并设置为找到的第一个非Visa项。现在我们必须检查卡的有效期不能设置得太早。

这个表单有两个JSpinner,一个用于卡的有效期日期,另一个用于卡的到期日期。我们不能使用名称来区分这两个微调器,所以我们必须使用反射来获取它们的引用。为此,我们首先找到包含微调器的最内部组件——在这个例子中是JPane,它是通过JTabbedPane的当前标签显示的。(Squish对某些小部件使用“代理”,在这个例子中是TabProxy;但我们可以像这里一样,始终使用component属性来访问相关组件。)一旦我们获得了JPane,我们就迭代其组件,制作一个包含JSpinner的列表。然后我们检查是否有恰好两个微调器,正如预期的那样,然后我们就可以检查最小有效日期是否已正确设置为三年前。

第三条业务规则指出,到期日期必须至少提前一个月。我们明确地将到期日期设置为提前35天,以便稍后启用Pay按钮。

def populateCardFields():
    cardAccountNameLineEdit = waitForObject(
        names.credit_Card_Account_Name_javax_swing_JTextField)
    type(cardAccountNameLineEdit, "An Account")
    cardAccountNumberLineEdit = waitForObject(
        names.credit_Card_Account_Number_javax_swing_JTextField)
    type(cardAccountNumberLineEdit, "1343 876 326 1323 32")
function populateCardFields()
{
    var cardAccountNameLineEdit = waitForObject(
        names.creditCardAccountNameJavaxSwingJTextField);
    type(cardAccountNameLineEdit, "An Account");
    var cardAccountNumberLineEdit = waitForObject(
        names.creditCardAccountNumberJavaxSwingJTextField);
    type(cardAccountNumberLineEdit, "1343 876 326 1323 32");
}
sub populateCardFields
{
    my $cardAccountNameLineEdit = waitForObject(
        $Names::credit_card_account_name_javax_swing_jtextfield);
    type($cardAccountNameLineEdit, "An Account");
    my $cardAccountNumberLineEdit = waitForObject(
        $Names::credit_card_account_number_javax_swing_jtextfield);
    type($cardAccountNumberLineEdit, "1343 876 326 1323 32");
}
def populateCardFields
    cardAccountNameLineEdit = waitForObject(
    Names::Credit_Card_Account_Name_javax_swing_JTextField)
    type(cardAccountNameLineEdit, "An Account")
    cardAccountNumberLineEdit = waitForObject(
    Names::Credit_Card_Account_Number_javax_swing_JTextField)
    type(cardAccountNumberLineEdit, "1343 876 326 1323 32")
end
proc populateCardFields {} {
    set cardAccountNameLineEdit [waitForObject \
        ":Credit Card.Account Name:_javax.swing.JTextField"]
    invoke type $cardAccountNameLineEdit "An Account"
    set cardAccountNumberLineEdit [waitForObject \
        ":Credit Card.Account Number:_javax.swing.JTextField"]
    invoke type $cardAccountNumberLineEdit "1343 876 326 1323 32"
}

最初应该禁用 支付 按钮,我们在 main 函数中检查是否禁用。对于第5条业务规则,我们需要一些假冒数据来填充卡片账户名称和号码,这由 populateCardFields 函数提供。完成调用后,由于日期现在在范围内,支付 按钮应该现在可以启用,我们再次在 main 函数中检查。

现在我们已经完成了使用有状态的单值小部件对测试业务规则的审查。Java有很多类似的组件,但所有这些都使用我们在这里使用的相同技术进行标识和测试。

如何测试 JList、JTable 和 JTree 组件(Java—AWT/Swing)

在本节中,我们将看到如何遍历 Java 的 JList、JTable 和 JTree 组件中的每个项目,以及如何检索每个项目的信息,例如它们的文本和选择状态。实际数据存储在模型中,因此在每个示例中,我们首先获取对组件底层模型的引用,然后对模型本身进行操作。

虽然示例仅将每个项目的文本和选择状态输出到 Squish 的日志中,但它们很容易适应执行更复杂的测试,例如比较实际值与预期值。

本节示出的所有代码均来自 examples/java/itemviews 示例的测试套件。

如何测试 JList

如以下测试示例所示,遍历 JList 中的所有项目并检索它们的文本和选择状态非常简单。

import os
def main():
    startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/java/itemviews/ItemViewsSwing.jar"')
    listWidgetName = ":Item Views_javax.swing.JList"
    listWidget = waitForObject(listWidgetName)
    model = listWidget.getModel()
    for row in range(model.getSize()):
        item = model.getElementAt(row)
        selected = ""
        if listWidget.isSelectedIndex(row):
            selected = " +selected"
        test.log("(%d) '%s'%s" % (row, item.toString(), selected))
function main()
{
    startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/java/itemviews/ItemViewsSwing.jar"');
    var listWidgetName = ":Item Views_javax.swing.JList";
    var listWidget = waitForObject(listWidgetName);
    var model = listWidget.getModel();
    for (var row = 0; row < model.getSize(); ++row) {
        var item = model.getElementAt(row);
        var selected = "";
        if (listWidget.isSelectedIndex(row)) {
            selected = " +selected";
        }
        test.log("(" + String(row) + ") '" + item.toString() + "'" + selected);
    }
}
sub main
{
    startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/java/itemviews/ItemViewsSwing.jar\"");
    my $listWidgetName = ":Item Views_javax.swing.JList";
    my $listWidget = waitForObject($listWidgetName);
    my $model = $listWidget->getModel();
    for (my $row = 0; $row < $model->getSize(); ++$row) {
        my $item = $model->getElementAt($row);
        my $selected = "";
        if ($listWidget->isSelectedIndex($row)) {
            $selected = " +selected";
        }
        test::log("($row) '" . $item->toString() . "'$selected");
    }
}
# encoding: UTF-8
require 'squish'
include Squish

def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/java/itemviews/ItemViewsSwing.jar\"")
    listWidgetName = ":Item Views_javax.swing.JList"
    listWidget = waitForObject(listWidgetName)
    model = listWidget.getModel()
    for row in 0...model.getSize()
        item = model.getElementAt(row)
        selected = ""
        if listWidget.isSelectedIndex(row)
            selected = " +selected"
        end
        Test.log("(#{row}) '#{item.toString()}'#{selected}")
    end
end
proc main {} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/java/itemviews/ItemViewsSwing.jar\""
    set listWidgetName ":Item Views_javax.swing.JList"
    set listWidget [waitForObject $listWidgetName]
    set model [invoke $listWidget getModel]
    for {set row 0} {$row < [invoke $model getSize]} {incr row} {
        set item [invoke $model getElementAt $row]
        set selected ""
        if {[invoke $listWidget isSelectedIndex $row]} {
            set selected " +selected"
        }
        set text [invoke $item toString]
        test log "($row) '$text'$selected"
    }
}

所有输出都输送到 Squish 的日志中,但显然很容易更改脚本以针对特定值进行测试等。

如何测试 JTable

如以下测试示例所示,遍历 JTable 中的所有项目并检索它们的文本和选择状态也非常简单。

import os
def main():
    startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/java/itemviews/ItemViewsSwing.jar"')
    tableWidgetName = ":Item Views_javax.swing.JTable"
    tableWidget = waitForObject(tableWidgetName)
    model = tableWidget.getModel()
    for row in range(model.getRowCount()):
        for column in range(model.getColumnCount()):
            item = model.getValueAt(row, column)
            selected = ""
            if tableWidget.isCellSelected(row, column):
                selected = " +selected"
            test.log("(%d, %d) '%s'%s" % (row, column, item.toString(), selected))
function main()
{
    startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/java/itemviews/ItemViewsSwing.jar"');
    var tableWidgetName = ":Item Views_javax.swing.JTable";
    var tableWidget = waitForObject(tableWidgetName);
    var model = tableWidget.getModel();
    for (var row = 0; row < model.getRowCount(); ++row) {
        for (var column = 0; column < model.getColumnCount(); ++column) {
            var item = model.getValueAt(row, column);
            var selected = "";
            if (tableWidget.isCellSelected(row, column)) {
                selected = " +selected";
            }
            test.log("(" + String(row) + ", " + String(column) + ") '" +
                     item.toString() + "'" + selected);
        }
    }
}
sub main
{
    startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/java/itemviews/ItemViewsSwing.jar\"");
    my $tableWidgetName = ":Item Views_javax.swing.JTable";
    my $tableWidget = waitForObject($tableWidgetName);
    my $model = $tableWidget->getModel();
    for (my $row = 0; $row < $model->getRowCount(); ++$row) {
        for (my $column = 0; $column < $model->getColumnCount(); ++$column) {
            my $item = $model->getValueAt($row, $column);
            my $selected = "";
            if ($tableWidget->isCellSelected($row, $column)) {
                $selected = " +selected";
            }
            test::log("($row, $column) '$item'$selected");
        }
    }
}
# encoding: UTF-8
require 'squish'
include Squish

def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/java/itemviews/ItemViewsSwing.jar\"")
    tableWidgetName = ":Item Views_javax.swing.JTable"
    tableWidget = waitForObject(tableWidgetName)
    model = tableWidget.getModel()
    for row in 0...model.getRowCount()
        for column in 0...model.getColumnCount()
            item = model.getValueAt(row, column)
            selected = ""
            if tableWidget.isCellSelected(row, column)
                selected = " +selected"
            end
            Test.log("(#{row}, #{column}) '#{item}'#{selected}")
        end
    end
end
proc main {} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/java/itemviews/ItemViewsSwing.jar\""
    set tableWidgetName ":Item Views_javax.swing.JTable"
    set tableWidget [waitForObject $tableWidgetName]
    set model [invoke $tableWidget getModel]
    for {set row 0} {$row < [invoke $model getRowCount]} {incr row} {
        for {set column 0} {$column < [invoke $model getColumnCount]} {incr column} {
            set item [invoke $model getValueAt $row $column]
            set selected ""
            if {[invoke $tableWidget isCellSelected $row $column]} {
                set selected " +selected"
            }
            set text [invoke $item toString]
            test log "($row, $column) '$text'$selected"
        }
    }
}

再次,所有输出都输送到 Squish 的日志中,显然很容易更改脚本以针对特定值进行测试等。

注意:您可以使用表格验证点来检查整个表格,就如在 如何创建和使用表格验证 中所述。

如何测试 JTree

遍历 JTree 中的所有项目并检索它们的文本和选择状态可能会稍微复杂一点——因为树是一个递归结构。尽管如此,这是完全可能的,如下面的测试示例所示。

    import os
    def checkAnItem(indent, model, item, selectionModel, treePath):
        if indent > -1:
            selected = ""
            if selectionModel.isPathSelected(treePath):
                selected = " +selected"
            test.log("|%s'%s'%s" % (" " * indent, item.toString(), selected))
        else:
            indent = -4
        for row in range(model.getChildCount(item)):
            child = model.getChild(item, row)
            childTreePath = treePath.pathByAddingChild(child)
            checkAnItem(indent + 4, model, child, selectionModel, childTreePath)

def main():
        startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/java/itemviews/ItemViewsSwing.jar"')
        treeWidgetName = ":Item Views_javax.swing.JTree"
        treeWidget = waitForObject(treeWidgetName)
        model = treeWidget.getModel()
        selectionModel = treeWidget.getSelectionModel()
        treePath = javax_swing_tree_TreePath(model.getRoot())
        checkAnItem(-1, model, model.getRoot(), selectionModel, treePath)
    function checkAnItem(indent, model, item, selectionModel, treePath)
    {
        if (indent > -1) {
            var selected = "";
            if (selectionModel.isPathSelected(treePath)) {
                selected = " +selected";
            }
            var offset = "";
            for (var i = 0; i < indent; ++i) {
                offset = offset.concat(" ");
            }
            test.log("|" + offset + "'" + item.toString() + "'" + selected);
        }
        else {
            indent = -4;
        }
        for (var row = 0; row < model.getChildCount(item); ++row) {
            var child = model.getChild(item, row);
            var childTreePath = treePath.pathByAddingChild(child);
            checkAnItem(indent + 4, model, child, selectionModel, childTreePath)
        }
    }

function main()
    {
        startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/java/itemviews/ItemViewsSwing.jar"');
        var treeWidgetName = ":Item Views_javax.swing.JTree";
        var treeWidget = waitForObject(treeWidgetName);
        var model = treeWidget.getModel();
        var selectionModel = treeWidget.getSelectionModel();
        var treePath = new javax_swing_tree_TreePath(model.getRoot());
        checkAnItem(-1, model, model.getRoot(), selectionModel, treePath);
    }
sub checkAnItem
{
    my ($indent, $model, $item, $selectionModel, $treePath) = @_;
    if ($indent > -1) {
        my $selected = "";
        if ($selectionModel->isPathSelected($treePath)) {
            $selected = " +selected";
        }
        my $padding = " " x $indent;
        test::log("|" . $padding . "'" . $item->toString() . "'$selected");
    }
    else {
        $indent = -4;
    }
    for (my $row = 0; $row < $model->getChildCount($item); ++$row) {
        my $child = $model->getChild($item, $row);
        my $childTreePath = $treePath->pathByAddingChild($child);
        checkAnItem($indent + 4, $model, $child, $selectionModel, $childTreePath);
    }
}

sub main
{
    startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/java/itemviews/ItemViewsSwing.jar\"");
    my $treeWidgetName = ":Item Views_javax.swing.JTree";
    my $treeWidget = waitForObject($treeWidgetName);
    my $model = $treeWidget->getModel();
    my $selectionModel = $treeWidget->getSelectionModel();
    my $treePath = new javax_swing_tree_TreePath($model->getRoot());
    checkAnItem(-1, $model, $model->getRoot(), $selectionModel, $treePath);
}
# encoding: UTF-8
require 'squish'
include Squish

def checkAnItem(indent, model, item, selectionModel, treePath)
    if indent > -1
        selected = ""
        if selectionModel.isPathSelected(treePath)
            selected = " +selected"
        end
        Test.log("|%s'%s'%s" % [" " * indent, item, selected])
    else
        indent = -4
    end
    for row in 0...model.getChildCount(item)
        child = model.getChild(item, row)
        childTreePath = treePath.pathByAddingChild(child)
        checkAnItem(indent + 4, model, child, selectionModel, childTreePath)
    end
end

def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/java/itemviews/ItemViewsSwing.jar\"")
    treeWidgetName = ":Item Views_javax.swing.JTree"
    treeWidget = waitForObject(treeWidgetName)
    model = treeWidget.getModel()
    selectionModel = treeWidget.getSelectionModel()
    treePath = LC::Javax_swing_tree_TreePath.new(model.getRoot())
    checkAnItem(-1, model, model.getRoot(), selectionModel, treePath)
end
    proc checkAnItem {indent model item selectionModel treePath} {
        if {$indent > -1} {
            set selected ""
            if {[invoke $selectionModel isPathSelected $treePath]} {
                set selected " +selected"
            }
            set offset [string repeat " " $indent]
            set text [invoke $item toString]
            test log "|$offset '$text'$selected"
        } else {
            set indent -4
        }
        for {set row 0} {$row < [invoke $model getChildCount $item]} {incr row} {
            set child [invoke $model getChild $item $row]
            set childTreePath [invoke $treePath pathByAddingChild $child]
            set offset [expr $indent + 4]
            checkAnItem $offset $model $child $selectionModel $childTreePath
        }
    }

proc main {} {
        startApplication "\"$::env(SQUISH_PREFIX)/examples/java/itemviews/ItemViewsSwing.jar\""
        set treeWidgetName ":Item Views_javax.swing.JTree"
        set treeWidget [waitForObject $treeWidgetName]
        set model [invoke $treeWidget getModel]
        set selectionModel [invoke $treeWidget getSelectionModel]
        set root [invoke $model getRoot]
        set treePath [construct javax_swing_tree_TreePath $root]
        checkAnItem -1 $model $root $selectionModel $treePath
    }

与 JList 和 JTable 的关键区别在于,由于 JTree 是递归的,所以我们自己使用递归遍历所有项目是最简单的。我们还跟踪了每个项目的“路径”,因为我们需要这些来确定哪些项目被选中——如果我们只想检索项目的属性,则不需要这样做。正如前面示例中一样,所有输出都输送到 Squish 的日志中,尽管很容易适配脚本以执行其他测试。

如何测试 JTable 并使用外部数据文件(Java—AWT/Swing)

本节我们将探讨如何测试以下所示的CsvTable.java程序。该程序使用JTable展示.csv(逗号分隔值)文件的内容,并提供一些基本的操作数据的功能——插入和删除行以及交换列。当我们复查测试时,我们将了解如何导入测试数据、操作数据,以及比较JTable显示的内容与我们预期内容的方式。由于CSV Table程序是一个主窗口式应用程序,我们还将学习如何测试菜单选项的行为是否如预期。

{}

CSV Table程序。

这个示例的源代码位于目录SQUISHDIR/examples/java/csvtable中,测试套件位于其下的子目录中——例如,测试的Python版本位于目录SQUISHDIR/examples/java/csvtable/suite_py中,测试的JavaScript版本位于SQUISHDIR/examples/java/csvtable/suite_js中等等。

我们将首先探讨的第一个测试很巧妙,仅仅是四个可执行语句。这种简单性是通过把几乎所有功能放入一个共享脚本中实现的,以避免代码重复。这里是完整的main函数:

import os

def main():
    startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/java/csvtable/CsvTableSwing.jar"')
    source(findFile("scripts", "common.py"))
    filename = "before.csv"
    doFileOpen(filename)
    jtable = waitForObject("{type='javax.swing.JTable' visible='true'}")
    compareTableWithDataFile(jtable, filename)
function main()
{
    startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/java/csvtable/CsvTableSwing.jar"');
    source(findFile("scripts", "common.js"));
    var filename = "before.csv";
    doFileOpen(filename);
    var jtable = waitForObject("{type='javax.swing.JTable' visible='true'}");
    compareTableWithDataFile(jtable, filename);
}
sub main
{
    startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/java/csvtable/CsvTableSwing.jar\"");
    source(findFile("scripts", "common.pl"));
    my $filename = "before.csv";
    doFileOpen($filename);
    my $jtable = waitForObject("{type='javax.swing.JTable' visible='true'}");
    compareTableWithDataFile($jtable, $filename);
}
# encoding: UTF-8
require 'squish'

include Squish

def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/java/csvtable/CsvTableSwing.jar\"")
    require findFile("scripts", "common.rb")
    filename = "before.csv"
    doFileOpen(filename)
    jtable = waitForObject("{type='javax.swing.JTable' visible='true'}")
    compareTableWithDataFile(jtable, filename)
end
proc main {} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/java/csvtable/CsvTableSwing.jar\""
    source [findFile "scripts" "common.tcl"]
    set filename "before.csv"
    doFileOpen $filename
    set jtable [waitForObject {{type='javax.swing.JTable' visible='true'}}]
    compareTableWithDataFile $jtable $filename
}

我们首先加载包含常见功能的脚本,就像我们在前一节中所做的那样。然后我们调用一个自定义的doFileOpen函数,告诉程序打开给定的文件——正如我们将看到的,这是通过用户界面进行的。接下来我们使用Object waitForObject(objectOrName)函数获取JTable的引用,最后我们检查JTable的内容是否与测试套件测试数据中保存的数据文件的内容匹配。请注意,CSV Table程序和Squish都使用他们各自的完全独立代码加载并解析数据文件。(参见如何创建和使用共享数据以及共享脚本了解如何将测试数据导入Squish。)

现在我们将探讨上述测试中使用的自定义函数。

def doFileOpen(filename):
    chooseMenuOptionByKey("F", "o")
    paneName = "{type='javax.swing.JRootPane' visible='true'}"
    waitForObject(paneName)
    # Platform-specific name
    fileDialogEntryName = names.open_File_Name_JTextField
    waitForObject(fileDialogEntryName)
    type(fileDialogEntryName, filename)
    waitForObject(fileDialogEntryName)
    type(fileDialogEntryName, "<Return>")

def chooseMenuOptionByKey(menuKey, optionKey):
    paneName = "{type='javax.swing.JRootPane' visible='true'}"
    waitForObject(paneName)
    type(paneName, "<Alt+%s>" % menuKey)
    waitForObject(paneName)
    type(paneName, optionKey)

def compareTableWithDataFile(jtable, filename):
    tableModel = jtable.getModel()
    for row, record in enumerate(testData.dataset(filename)):
        for column, name in enumerate(testData.fieldNames(record)):
            text = tableModel.getValueAt(row, column).toString()
            test.compare(testData.field(record, name), text)
function doFileOpen(filename)
{
    chooseMenuOptionByKey("F", "o");
    var paneName = "{type='javax.swing.JRootPane' visible='true'}";
    waitForObject(paneName);
    // Platform-specific name
    var fileDialogEntryName = names.openFileNameJTextField;
    waitForObject(fileDialogEntryName);
    type(fileDialogEntryName, filename);
    waitForObject(fileDialogEntryName);
    type(fileDialogEntryName, "<Return>");
}

function chooseMenuOptionByKey(menuKey, optionKey)
{
    var paneName = "{type='javax.swing.JRootPane' visible='true'}";
    waitForObject(paneName);
    type(paneName, "<Alt+" + menuKey + ">")
    waitForObject(paneName);
    type(paneName, optionKey);
}

function compareTableWithDataFile(jtable, filename)
{
    var tableModel = jtable.getModel();
    var records = testData.dataset(filename);
    for (var row = 0; row < records.length; ++row) {
        columnNames = testData.fieldNames(records[row]);
        for (var column = 0; column < columnNames.length; ++column) {
            text = tableModel.getValueAt(row, column).toString();
            test.compare(testData.field(records[row], column), text);
        }
    }
}
sub doFileOpen
{
    my $filename = shift(@_);
    chooseMenuOptionByKey("F", "o");
    my $paneName = "{type='javax.swing.JRootPane' visible='true'}";
    waitForObject($paneName);
    # Platform-specific name
    my $fileDialogEntryName = ("{leftWidget=':Open.File Name:" .
        "_javax.swing.plaf.metal.MetalFileChooserUI\$AlignedLabel' " .
        "type='javax.swing.plaf.metal.MetalFileChooserUI\$3' " .
        "visible='true' window=':Open_javax.swing.JDialog'}");
    waitForObject($fileDialogEntryName);
    type($fileDialogEntryName, $filename);
    waitForObject($fileDialogEntryName);
    type($fileDialogEntryName, "<Return>");
}

sub chooseMenuOptionByKey
{
    my($menuKey, $optionKey) = @_;
    my $paneName = "{type='javax.swing.JRootPane' visible='true'}";
    waitForObject($paneName);
    type($paneName, "<Alt+$menuKey>");
    waitForObject($paneName);
    type($paneName, $optionKey);
}

sub compareTableWithDataFile
{
    my ($jtable, $filename) = @_;
    my $tableModel = $jtable->getModel();
    my @records = testData::dataset($filename);
    for (my $row = 0; $row < scalar(@records); $row++) {
        my @columnNames = testData::fieldNames($records[$row]);
        for (my $column = 0; $column < scalar(@columnNames); $column++) {
            my $text = $tableModel->getValueAt($row, $column)->toString();
            test::compare($text, testData::field($records[$row], $column));
        }
    }
}
def doFileOpen(filename)
    chooseMenuOptionByKey("F", "o")
    paneName = "{type='javax.swing.JRootPane' visible='true'}"
    waitForObject(paneName)
    # Platform-specific name
    fileDialogEntryName = Names::Open_File_Name_JTextField
    waitForObject(fileDialogEntryName)
    type(fileDialogEntryName, filename)
    waitForObject(fileDialogEntryName)
    type(fileDialogEntryName, "<Return>")
end

def chooseMenuOptionByKey(menuKey, optionKey)
    paneName = "{type='javax.swing.JRootPane' visible='true'}"
    waitForObject(paneName)
    type(paneName, "<Alt+#{menuKey}>")
    waitForObject(paneName)
    type(paneName, optionKey)
end

def compareTableWithDataFile(jtable, filename)
    tableModel = jtable.getModel()
    TestData.dataset(filename).each_with_index do
        |record, row|
        for column in 0...TestData.fieldNames(record).length
            text = tableModel.getValueAt(row, column).toString()
            Test.compare(TestData.field(record, column), text)
        end
    end
end
proc doFileOpen {filename} {
    chooseMenuOptionByKey "F" "o"
    set paneName {{type='javax.swing.JRootPane' visible='true'}}
    waitForObject $paneName
    # Platform-specific name
    set fileDialogEntryName \
        {{leftWidget=':Open.File Name:_javax.swing.plaf.metal.MetalFileChooserUI$AlignedLabel' type='javax.swing.plaf.metal.MetalFileChooserUI$3' visible='true' window=':Open_javax.swing.JDialog'}}
    waitForObject $fileDialogEntryName
    invoke type $fileDialogEntryName $filename
    waitForObject $fileDialogEntryName
    invoke type $fileDialogEntryName "<Return>"
}

proc chooseMenuOptionByKey {menuKey optionKey} {
    set paneName {{type='javax.swing.JRootPane' visible='true'}}
    waitForObject $paneName
    invoke type $paneName "<Alt+$menuKey>"
    waitForObject $paneName
    invoke type $paneName $optionKey
}

proc compareTableWithDataFile {jtable filename} {
    set data [testData dataset $filename]
    set tableModel [invoke $jtable getModel]
    for {set row 0} {$row < [llength $data]} {incr row} {
        set columnNames [testData fieldNames [lindex $data $row]]
        for {set column 0} {$column < [llength $columnNames]} \
            {incr column} {
            set item [invoke $tableModel getValueAt $row $column]
            test compare [testData field [lindex $data $row] $column] \
                [invoke $item toString]
        }
    }
}

doFileOpen函数首先通过用户界面打开一个文件。这是通过使用自定义的chooseMenuOptionByKey函数实现的。所使用的文件对话框可能在所有平台上都不相同,所以文本输入的名称(在这种情况下是类型为AlignedLabel)可能会有所不同,因此我们添加了一个注意,这个名字是平台特定的。除了以外,使用对话框本身很简单——我们只需在文本输入框中键入文件名,然后按Return键确认。

chooseMenuOptionByKey函数模拟用户点击Alt+k(其中k是字符,例如 "F" 对应文件菜单),然后是对应所需操作的字符(例如,"o" 对应 "Open")。

当文件打开时,程序应该加载文件的数据。我们通过比较JTable中显示的数据和文件中的数据来检查数据是否正确加载。这种比较是通过自定义的compareTableWithDataFile函数进行的。此函数使用Squish的Dataset testData.dataset(filename)函数加载数据,以便可以通过Squish API访问它。我们希望表中的每个单元格都匹配对应的数据项,并使用Boolean test.compare(value1, value2)函数检查这种情况。

既然我们知道了如何比较表中的数据与文件中的数据,我们可以进行一些更雄心勃勃的测试。我们将加载 before.csv 文件,删除几行,在中间插入一行新数据,并在末尾追加一行新数据。然后我们将交换几对列。最后,数据应与 after.csv 文件匹配。

与其编写代码来完成所有这些操作,我们可以简单地记录一个测试脚本,打开文件并执行所有删除、插入和列交换操作。然后我们可以编辑已记录的测试脚本来在末尾添加几行代码,以比较实际结果与预期结果。下面是测试脚本的一部分,从手写代码的上一行开始,一直持续到脚本的末尾。

    source(findFile("scripts", "common.py"))
    jtable = waitForObject("{type='javax.swing.JTable' visible='true'}")
    tableModel = jtable.getModel()
    test.verify(tableModel.getColumnCount() == 5)
    test.verify(tableModel.getRowCount() == 10)
    compareTableWithDataFile(jtable, "after.csv")
# End of Added by Hand
    type(waitForObject(names.csv_Table_before_csv_JRootPane), "<Alt+F>")
    type(waitForObject(names.csv_Table_before_csv_JRootPane), "q")
    type(waitForObject(names.csv_Table_Yes_JButton), "<Alt+N>")
    source(findFile("scripts", "common.js"))
    var jtable = waitForObject("{type='javax.swing.JTable' visible='true'}")
    var tableModel = jtable.getModel()
    test.verify(tableModel.getColumnCount() == 5)
    test.verify(tableModel.getRowCount() == 10)
    compareTableWithDataFile(jtable, "after.csv")
// End of Added by Hand
    type(waitForObject(names.csvTableBeforeCsvJRootPane), "<Alt+F>");
    type(waitForObject(names.csvTableBeforeCsvJRootPane), "q");
    type(waitForObject(names.csvTableYesJButton), "<Alt+N>");
}
    source(findFile("scripts", "common.pl"));
    my $jtable = waitForObject("{type='javax.swing.JTable' visible='true'}");
    my $tableModel = $jtable->getModel();
    test::verify($tableModel->getColumnCount() == 5);
    test::verify($tableModel->getRowCount() == 10);
    compareTableWithDataFile($jtable, "after.csv");
# End of Added by Hand
    type(waitForObject(":CSV Table - before.csv_JRootPane"), "<Alt+F>");
    type(waitForObject(":CSV Table - before.csv_JRootPane"), "q");
    type(waitForObject(":CSV Table.Yes_JButton"), "<Alt+N>");
}
    require findFile("scripts", "common.rb")
    jtable = waitForObject("{type='javax.swing.JTable' visible='true'}")
    tableModel = jtable.getModel()
    Test.verify(tableModel.getColumnCount() == 5)
    Test.verify(tableModel.getRowCount() == 10)
    compareTableWithDataFile(jtable, "after.csv")
    # End of Added by Hand
    type(waitForObject(Names::CSV_Table_before_csv_JRootPane), "<Alt+F>")
    type(waitForObject(Names::CSV_Table_before_csv_JRootPane), "q")
    type(waitForObject(Names::CSV_Table_Yes_JButton), "<Alt+N>")
end
    source [findFile "scripts" "common.tcl"]
    set jtable [waitForObject {{type='javax.swing.JTable' \
        visible='true'}}]
    set tableModel [invoke $jtable getModel]
    test compare [invoke $tableModel getColumnCount] 5
    test compare [invoke $tableModel getRowCount] 10
    compareTableWithDataFile $jtable "after.csv"
# End of Added by Hand
    invoke type [waitForObject ":CSV Table - before.csv_JRootPane"] "<Alt+F>"
    invoke type [waitForObject ":CSV Table - before.csv_JRootPane"] "q"
    invoke type [waitForObject ":CSV Table.Yes_JButton"] "<Alt+N>"
}

如摘要所示,这些添加的行并不是插入在已记录测试脚本的 末尾,而是在程序终止之前——毕竟,我们需要程序正在运行来查询其 JTable。 (行数不同的原因是记录每个脚本的交互均略有不同。)

此示例展示了记录与手动编辑相结合的强大功能。如果将来程序中添加了新功能,我们可以以多种方式将其包含在测试中。最简单的方法是添加另一个测试脚本,记录,然后添加比较表与预期数据的所需行。另一种方法是在临时测试中记录对新特性的使用,然后将记录复制并粘贴到现有测试的合适位置,然后将文件更改为在末尾比较的文件,该文件考虑了原始数据的所有更改以及使用新功能产生的更改。或者,我们可以直接将测试片段记录到现有测试中。

如何测试 Java SWT 应用程序

在以下小节中,我们将重点关注测试 Java SWT 小部件,包括类似按钮和日期/时间编辑的单值小部件以及类似列表、表格和树的多元值小部件。我们还将介绍使用外部数据文件进行测试的内容。

如何测试有状态的单值小部件(Java/SWT)

在本节中,我们将了解如何测试 examples/java/paymentform_swt/PaymentFormSWT.java 示例程序。此程序使用许多基本的 Java/SWT 小部件,包括 ButtonComboDateTimeTabFolderText。作为对本示例的覆盖部分,我们将展示如何检查单个小部件的值和状态。我们还将展示如何测试表单的业务规则。

{}

PaymentFormSWT 在“信用卡支付”模式下的示例。

当需要支付发票时(无论是销售点,还是信用卡通过电话支付),将调用 PaymentFormSWT。表单的 Pay 按钮必须在正确字段填写且具有有效值时才可启用。我们必须测试的业务规则如下

  • 在"现金"模式下,即当选中现金选项卡时:
    • 最低还款金额为一美元,最高为2000美元或应付款项,取较小者。
  • 在"支票"模式下,即当选中支票选项卡时:
    • 最低还款金额为10美元或应付款项的5%,取较大者,最高为250美元或应付款项,取较小者。
    • 检查日期必须在 30 天前之后且不超过明天,并且必须最初设置为今天的日期。
    • 银行名称、银行号码、账号和账号行编辑必须都不为空。
    • 已签署支票复选框必须被选中。
  • 在"卡"模式下,即当选中信用卡选项卡时:
    • 最低还款金额为10美元或应付款项的5%,取较大者,最高为5000美元或应付款项,取较小者。
    • 发行日期必须至少在前三年之前,并且必须最初设置为最早可能的日期。
    • 到期日期必须比今天晚至少一个月,并且必须最初设置为最早可能的日期。
    • 账号和账号行编辑必须不为空。

注意:Java/SWT的Eclipse 3.4版本的DateTime控制不支持日期范围的设置,因此虽然应用程序通过监听器限制了日期范围,我们无法轻松地在手写代码中测试这一点。一个简单的解决方案是记录一个脚本,尝试更改超出范围的日期,然后要么作为一个单独的测试运行,要么复制并粘贴相关的行到手写脚本中。

我们将编写三个测试,每个针对表单的每种模式。

支付表单的源代码在目录SQUISHDIR/examples/java/paymentform_swt中,而测试套件在下面的子目录中——例如,测试的Python版本在目录SQUISHDIR/examples/java/paymentform_swt/suite_py中,而测试的JavaScript版本在SQUISHDIR/examples/java/paymentform_swt/suite_js中,等等。

我们首先将审查测试脚本,用于测试表单的“现金”模式。首先我们展示代码,然后解释它。

import os
def main():
    startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/java/paymentform_swt/PaymentFormSWT.jar"')
    # Start with the correct tab
    tabFolderName = ":Payment Form_org.eclipse.swt.widgets.TabFolder"
    tabFolder = waitForObject(tabFolderName)
    clickTab(tabFolder, "C&ash")

    # Business rule #1: the minimum payment is $1 and the maximum is
    # $2000 or the amount due whichever is smaller
    amountDueLabelName = ("{caption?='[$][0-9.,]*' "
        "type='org.eclipse.swt.widgets.Label' visible='true' "
        "window=':Payment Form_org.eclipse.swt.widgets.Shell'}")
    amountDueLabel = waitForObject(amountDueLabelName)
    chars = []
    for char in str(amountDueLabel.getText()):
        if char.isdigit():
            chars.append(char)
    amount_due = cast("".join(chars), int)
    maximum = min(2000, amount_due)
    paymentSpinnerName = ("{isvisible='true' "
        "type='org.eclipse.swt.widgets.Spinner' "
        "window=':Payment Form_org.eclipse.swt.widgets.Shell'}")
    paymentSpinner = waitForObject(paymentSpinnerName)
    test.verify(paymentSpinner.getMinimum() == 1)
    test.verify(paymentSpinner.getMaximum() == maximum)

# Business rule #2: the Pay button is enabled (since the above tests
    # ensure that the payment amount is in range)
    payButtonName = ":Payment Form.Pay_org.eclipse.swt.widgets.Button"
    payButton = waitForObject(payButtonName)
    test.verify(payButton.isEnabled())
function main()
{
    startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/java/paymentform_swt/PaymentFormSWT.jar"');
    // Start with the correct tab
    var tabFolderName = ":Payment Form_org.eclipse.swt.widgets.TabFolder";
    var tabFolder = waitForObject(tabFolderName);
    clickTab(tabFolder, "C&ash");

    // Business rule #1: the minimum payment is $1 and the maximum is
    // $2000 or the amount due whichever is smaller
    var amountDueLabelName = "{caption?='[$][0-9.,]*' " +
        "type='org.eclipse.swt.widgets.Label' " +
        "window=':Payment Form_org.eclipse.swt.widgets.Shell'}";
    var amountDueLabel = waitForObject(amountDueLabelName);
    var amount_due = 0 + amountDueLabel.text.replace(/\D/g, "");
    var maximum = Math.min(2000, amount_due);

    var paymentSpinnerName = "{isvisible='true' " +
        "type='org.eclipse.swt.widgets.Spinner' " +
        "window=':Payment Form_org.eclipse.swt.widgets.Shell'}";
    var paymentSpinner = waitForObject(paymentSpinnerName);
    test.verify(paymentSpinner.getMinimum() == 1);
    test.verify(paymentSpinner.getMaximum() == maximum);

// Business rule #2: the Pay button is enabled (since the
    // above tests ensure that the payment amount is in range)
    var payButtonName = ":Payment Form.Pay_org.eclipse.swt." +
        "widgets.Button";
    var payButton = waitForObject(payButtonName);
    test.verify(payButton.isEnabled());
}
sub main
{
    startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/java/paymentform_swt/PaymentFormSWT.jar\"");
    # Start with the correct tab
    my $tabFolderName = ":Payment Form_org.eclipse.swt.widgets.TabFolder";
    my $tabFolder = waitForObject($tabFolderName);
    clickTab($tabFolder, "C&ash");

    # Business rule #1: the minimum payment is $1 and the maximum is
    # $2000 or the amount due whichever is smaller
    my $amountDueLabelName = "{caption?='[\$][0-9.,]*' " .
        "type='org.eclipse.swt.widgets.Label' visible='true' " .
        "window=':Payment Form_org.eclipse.swt.widgets.Shell'}";
    my $amountDueLabel = waitForObject($amountDueLabelName);
    my $amount_due = $amountDueLabel->text;
    $amount_due =~ s/\D//g; # remove non-digits
    my $maximum = 2000 < $amount_due ? 2000 : $amount_due;
    my $paymentSpinnerName = "{isvisible='true' " .
        "type='org.eclipse.swt.widgets.Spinner' " .
        "window=':Payment Form_org.eclipse.swt.widgets.Shell'}";
    my $paymentSpinner = waitForObject($paymentSpinnerName);
    test::verify($paymentSpinner->getMinimum() == 1);
    test::verify($paymentSpinner->getMaximum() == $maximum);

    # Business rule #2: the Pay button is enabled (since the above tests
    # ensure that the payment amount is in range)
    my $payButtonName = ":Payment Form.Pay_org.eclipse.swt.widgets.Button";
    my $payButton = waitForObject($payButtonName);
    test::verify($payButton->isEnabled());
}
# encoding: UTF-8
require 'squish'
include Squish

def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/java/paymentform_swt/PaymentFormSWT.jar\"")
    # Start with the correct tab
    tabFolderName = ":Payment Form_org.eclipse.swt.widgets.TabFolder"
    tabFolder = waitForObject(tabFolderName)
    clickTab(tabFolder, "C&ash")

    # Business rule #1: the minimum payment is $1 and the maximum is
    # $2000 or the amount due whichever is smaller
    amountDueLabelName = "{caption?='[$][0-9.,]*' " +
    "type='org.eclipse.swt.widgets.Label' visible='true' " +
    "window=':Payment Form_org.eclipse.swt.widgets.Shell'}"
    amountDueLabel = waitForObject(amountDueLabelName)
    amount_due = String(amountDueLabel.text).gsub(/\D/, "").to_f
    maximum = 2000 < amount_due ? 2000 : amount_due
    paymentSpinnerName = "{isvisible='true' " +
    "type='org.eclipse.swt.widgets.Spinner' " +
    "window=':Payment Form_org.eclipse.swt.widgets.Shell'}"
    paymentSpinner = waitForObject(paymentSpinnerName)
    Test.verify(paymentSpinner.getMinimum() == 1)
    Test.verify(paymentSpinner.getMaximum() == maximum)

    # Business rule #2: the Pay button is enabled (since the above tests
    # ensure that the payment amount is in range)
    payButtonName = ":Payment Form.Pay_org.eclipse.swt.widgets.Button"
    payButton = waitForObject(payButtonName)
    Test.verify(payButton.isEnabled())
end
proc main {} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/java/paymentform_swt/PaymentFormSWT.jar\""
    # Start with the correct tab
    set tabFolderName ":Payment Form_org.eclipse.swt.widgets.TabFolder"
    set tabFolder [waitForObject $tabFolderName]
    invoke clickTab $tabFolder "C&ash"

    # Business rule #1: the minimum payment is $1 and the maximum is
    # $2000 or the amount due whichever is smaller
    set amountDueLabelName {{caption?='[$][0-9.,]*' \
        type='org.eclipse.swt.widgets.Label' \
        window=':Payment Form_org.eclipse.swt.widgets.Shell'}}
    set amountDueLabel [waitForObject $amountDueLabelName]
    set amountText [toString [property get $amountDueLabel text]]
    regsub -all {\D} $amountText "" amountText
    set amount_due [expr $amountText]
    set maximum [expr $amount_due < 2000 ? $amount_due : 2000]
    set paymentSpinnerName {{isvisible='true' \
        type='org.eclipse.swt.widgets.Spinner' \
        window=':Payment Form_org.eclipse.swt.widgets.Shell'}}
    set paymentSpinner [waitForObject $paymentSpinnerName]
    test compare [invoke $paymentSpinner getMinimum] 1
    test compare [invoke $paymentSpinner getMaximum] $maximum

# Business rule #2: the Pay button is enabled (since the above tests
    # ensure that the payment amount is in range)
    set payButtonName ":Payment Form.Pay_org.eclipse.swt.widgets.Button"
    set payButton [waitForObject $payButtonName]
    test verify [invoke $payButton isEnabled]
}

我们必须首先确保表单处于我们想要测试的模式。一般来说,我们获取可见小部件的方法总是相同的:我们创建一个包含小部件名称的变量,然后调用Object waitForObject(objectOrName)来获取小部件的引用。一旦我们获得引用,我们就可以用它来访问小部件的属性和调用小部件的方法。在这种情况下,我们使用Object waitForObject(objectOrName)函数来获取TabFolder小部件的引用,然后使用clickTab(objectOrName, tabText)函数来点击我们感兴趣的工具选项卡。我们是如何知道工具选项卡栏的名称的?我们使用了Spy(见如何使用Spy)。

要测试的第一个业务规则涉及到允许支付的最小和最大金额。像往常一样,我们开始调用Object waitForObject(objectOrName)来获取我们感兴趣的部件的引用——在这种情况下,从应付标签开始。因为应付标签的文本根据金额的不同而变化,所以我们不能给它一个固定的名称。因此,我们使用真正的(多属性)名称通过通配符来识别它。[$][0-9.,]*的通配符匹配任何以美元符号开头后跟零个或多个数字、圆点和逗号的文本。Squish还可以进行正则表达式匹配——关于匹配的更多信息,请参见改进对象识别

由于标签的文本可能包含货币符号和分组标记(例如,$1,700或€1.700),为了将其文本转换为整数,我们必须首先删除任何非数字字符。我们根据底层脚本语言以不同的方式完成此操作。(例如,在Python中,我们遍历每个字符,并将所有数字字符联合成单个字符串,并使用Object cast(object, type)函数,该函数接受一个对象以及期望将对象转换为的类型,并返回所需类型的对象——或失败时返回0。在JavaScript中,我们采用类似的方法,但对于Perl和Tcl,我们只是使用正则表达式删除非数字字符。)得到的整数是应付金额,因此我们现在可以简单地计算现金支付的最高金额。

知道了最小和最大金额后,接下来获取支付Spinner的引用,同样使用Spy来找出旋转器的名称。一旦我们有了旋转器的引用,我们就使用Boolean test.verify(condition)方法来确保设置了正确的最小和最大金额。(对于Tcl,我们使用了Boolean test.compare(value1, value2)方法而不是Boolean test.verify(condition),因为它更方便。)

在这种情况下,检查最后一个业务规则很容易,因为如果金额在范围内(而且它必须在,因为我们刚刚检查过),那么支付被允许,所以支付按钮应该处于启用状态。同样,我们使用同样的方法来测试这一点:首先调用Object waitForObject(objectOrName)来获取它的引用,然后进行测试——在这种情况下,检查支付按钮是否启用。

尽管“现金”模式测试效果良好,但代码几乎相同的几个位置。因此,在创建“支票”和“卡片”模式的测试之前,我们将创建一些通用的函数,我们可以使用它们来重构测试。(用于创建共享代码的过程在下文中略有说明如何创建和使用共享数据和共享脚本——我们基本上需要做的是在测试套件的共享项脚本中创建一个新的脚本。)Python通用代码在common.py中,JavaScript通用代码在common.js中,依此类推。

    def clickTabItem(name):
        tabFolderName = ("{isvisible='true' "
            "type='org.eclipse.swt.widgets.TabFolder' "
            "window=':Payment Form_org.eclipse.swt.widgets.Shell'}")
        tabFolder = waitForObject(tabFolderName)
        clickTab(tabFolder, name)


    def dateTimeEqualsDate(dateTime, date):
        return (dateTime.getYear() == date.get(java_util_Calendar.YEAR) and
            dateTime.getMonth() == date.get(java_util_Calendar.MONTH) and
            dateTime.getDay() == date.get(java_util_Calendar.DAY_OF_MONTH))


def getAmountDue():
        amountDueLabelName = ("{caption?='[$][0-9.,]*' "
                "type='org.eclipse.swt.widgets.Label' visible='true' "
                "window=':Payment Form_org.eclipse.swt.widgets.Shell'}")
        amountDueLabel = waitForObject(amountDueLabelName)
        chars = []
        for char in str(amountDueLabel.getText()):
            if char.isdigit():
                chars.append(char)
        return cast("".join(chars), int)


    def checkPaymentRange(minimum, maximum):
        paymentSpinner = waitForObject("{isvisible='true' "
            "type='org.eclipse.swt.widgets.Spinner' "
            "window=':Payment Form_org.eclipse.swt.widgets.Shell'}")
        test.verify(paymentSpinner.getMinimum() == minimum)
        test.verify(paymentSpinner.getMaximum() == maximum)
    function clickTabItem(name)
    {
        var tabFolderName = "{isvisible='true' type='org.eclipse." +
            "swt.widgets.TabFolder' " +
            "window=':Payment Form_org.eclipse.swt.widgets.Shell'}";
        var tabFolder = waitForObject(tabFolderName);
        clickTab(tabFolder, name);
    }


    function dateTimeEqualsDate(dateTime, aDate)
    {
        return (dateTime.getYear() == aDate.get(java_util_Calendar.YEAR) &&
            dateTime.getMonth() == aDate.get(java_util_Calendar.MONTH) &&
            dateTime.getDay() == aDate.get(java_util_Calendar.DAY_OF_MONTH));
    }


function getAmountDue()
    {
        var amountDueLabel = waitForObject("{caption?='[$][0-9.,]*' " +
                "type='org.eclipse.swt.widgets.Label' visible='true' " +
                "window=':Payment Form_org.eclipse.swt.widgets.Shell'}");
        return 0 + amountDueLabel.text.replace(/\D/g, "");
    }


    function checkPaymentRange(minimum, maximum)
    {
        var paymentSpinner = waitForObject("{isvisible='true' " +
            "type='org.eclipse.swt.widgets.Spinner' " +
            "window=':Payment Form_org.eclipse.swt.widgets.Shell'}");
        test.verify(paymentSpinner.getMinimum() == minimum);
        test.verify(paymentSpinner.getMaximum() == maximum);
    }
    sub clickTabItem
    {
        my $name = shift(@_);
        my $tabFolderName = "{isvisible='true' " .
            "type='org.eclipse.swt.widgets.TabFolder' " .
            "window=':Payment Form_org.eclipse.swt.widgets.Shell'}";
        my $tabFolder = waitForObject($tabFolderName);
        clickTab($tabFolder, $name);
    }


    sub dateTimeEqualsDate
    {
        my ($dateTime, $date) = @_;
        return ($dateTime->getYear() == $date->get(java_util_Calendar->YEAR) &&
            $dateTime->getMonth() == $date->get(java_util_Calendar->MONTH) &&
            $dateTime->getDay() == $date->get(java_util_Calendar->DAY_OF_MONTH));
    }


sub getAmountDue
    {
        my $amountDueLabel = waitForObject("{caption?='[\$][0-9.,]*' " .
                "type='org.eclipse.swt.widgets.Label' visible='true' " .
                "window=':Payment Form_org.eclipse.swt.widgets.Shell'}");
        my $amount_due = $amountDueLabel->text;
        $amount_due =~ s/\D//g; # remove non-digits
        return $amount_due;
    }


    sub checkPaymentRange
    {
        my ($minimum, $maximum) = @_;
        my $paymentSpinner = waitForObject("{isvisible='true' " .
            "type='org.eclipse.swt.widgets.Spinner' " .
            "window=':Payment Form_org.eclipse.swt.widgets.Shell'}");
        test::verify($paymentSpinner->getMinimum() == $minimum);
        test::verify($paymentSpinner->getMaximum() == $maximum);
    }
# encoding: UTF-8
require 'squish'

include Squish

def clickTabItem(name)
    tabFolderName = "{isvisible='true' " +
    "type='org.eclipse.swt.widgets.TabFolder' " +
    "window=':Payment Form_org.eclipse.swt.widgets.Shell'}"
    tabFolder = waitForObject(tabFolderName)
    clickTab(tabFolder, name)
end

def dateTimeEqualsDate(dateTime, date)
    dateTime.getYear() == date.get(LC::Java_util_Calendar.YEAR) &&
    dateTime.getMonth() == date.get(LC::Java_util_Calendar.MONTH) &&
    dateTime.getDay() == date.get(LC::Java_util_Calendar.DAY_OF_MONTH)
end

def getAmountDue
    amountDueLabelName = "{caption?='[$][0-9.,]*' " +
    "type='org.eclipse.swt.widgets.Label' visible='true' " +
    "window=':Payment Form_org.eclipse.swt.widgets.Shell'}"
    amountDueLabel = waitForObject(amountDueLabelName)
    String(amountDueLabel.text).gsub(/\D/, "").to_f
end

def checkPaymentRange(minimum, maximum)
    paymentSpinner = waitForObject("{isvisible='true' " +
    "type='org.eclipse.swt.widgets.Spinner' " +
    "window=':Payment Form_org.eclipse.swt.widgets.Shell'}")
    Test.verify(paymentSpinner.getMinimum() == minimum)
    Test.verify(paymentSpinner.getMaximum() == maximum)
end

def min(a, b)
    a < b ? a : b
end

def max(a, b)
    a < b ? b : a
end
    proc clickTabItem {name} {
        set tabFolderName {{isvisible='true' \
            type='org.eclipse.swt.widgets.TabFolder' \
            window=':Payment Form_org.eclipse.swt.widgets.Shell'}}
        set tabFolder [waitForObject $tabFolderName]
        invoke clickTab $tabFolder $name
    }


    proc getAmountDue {} {
        set amountDueLabelName {{caption?='[$][0-9.,]*' \
            type='org.eclipse.swt.widgets.Label' \
            window=':Payment Form_org.eclipse.swt.widgets.Shell'}}
        set amountDueLabel [waitForObject $amountDueLabelName]
        set amountText [toString [property get $amountDueLabel text]]
        regsub -all {\D} $amountText "" amountText
        return [expr $amountText]
    }


    proc dateTimeEqualsDate {dateTime date} {
        set yearsMatch [expr [invoke $dateTime getYear] == \
            [invoke $date get [property get java_util_Calendar YEAR]]]
        set monthsMatch [expr [invoke $dateTime getMonth] == \
            [invoke $date get [property get java_util_Calendar MONTH]]]
        set daysMatch [expr [invoke $dateTime getDay] == \
            [invoke $date get [property get java_util_Calendar DAY_OF_MONTH]]]
        if {$yearsMatch && $monthsMatch && $daysMatch} {
            return true
        }
        return false
    }


    proc checkPaymentRange {minimum maximum} {
        set paymentSpinner [waitForObject {{isvisible='true' \
            type='org.eclipse.swt.widgets.Spinner' \
            window=':Payment Form_org.eclipse.swt.widgets.Shell'}}]
        test compare [invoke $paymentSpinner getMinimum] $minimum
        test compare [invoke $paymentSpinner getMaximum] $maximum

}

现在我们可以为“检查”模式和“卡片”模式编写测试,并更多地投入到业务规则的测试中,而将更多精力放在一些基本任务上。 “检查”模式的代码相当长,但是我们将它分解为一个main函数——Squish唯一会调用的函数——以及几个针对特定测试的支持函数,这些支持函数帮助使main函数简短且清晰,同时使用了上面看到的一些常用函数。

def main():
    startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/java/paymentform_swt/PaymentFormSWT.jar"')
    # Import functionality needed by more than one test script
    source(findFile("scripts", "common.py"))

    # Start with the correct tab
    clickTabItem("Chec&k")

    # Business rule #1: the minimum payment is $10 and the maximum is
    # $250 or the amount due whichever is smaller
    amount_due = getAmountDue()
    checkPaymentRange(10, min(250, amount_due))

    # Business rule #2: the check date must be no earlier than 30 days
    # ago and no later than tomorrow, and must initially be set to
    # today. Here we just check its initial value.
    checkDateTime = waitForObject(names.check_Check_Date_DateTime)
    today = java_util_Calendar.getInstance()
    test.verify(dateTimeEqualsDate(checkDateTime, today))

    # Business rule #3: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use waitForObjectExists()
    payButtonName = names.payment_Form_Pay_org_eclipse_swt_widgets_Button
    payButton = waitForObjectExists(payButtonName)
    test.verify(not payButton.isEnabled())

    # Business rule #4: the check must be signed (and if it isn't we
    # will check the check box ready to test the next rule)
    ensureSignedCheckBoxIsChecked()

    # Business rule #5: the Pay button should be enabled since all the
    # previous tests pass, the check is signed and now we have filled in
    # the account details
    populateCheckFields()
    payButton = waitForObject(payButtonName)
    test.verify(payButton.isEnabled())
function main()
{
    startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/java/paymentform_swt/PaymentFormSWT.jar"');
    // Import functionality needed by more than one test script
    source(findFile("scripts", "common.js"));

    // Start with the correct tab
    clickTabItem("Chec&k");

    // Business rule #1: the minimum payment is $10 and the maximum is
    // $250 or the amount due whichever is smaller
    var amount_due = getAmountDue();
    checkPaymentRange(10, Math.min(250, amount_due));

    // Business rule #2: the check date must be no earlier than 30 days
    // ago and no later than tomorrow, and must initially be set to
    // today. Here we just check its initial value.
    var checkDateTime = waitForObject(names.checkCheckDateDateTime);
    var today = java_util_Calendar.getInstance();
    test.verify(dateTimeEqualsDate(checkDateTime, today));

    // Business rule #3: the Pay button is disabled (since the form's data
    // isn't yet valid), so we use waitForObjectExists()
    var payButtonName = names.paymentFormPayOrgEclipseSwtWidgetsButton;
    var payButton = waitForObjectExists(payButtonName);
    test.verify(!payButton.isEnabled());

    // Business rule #4: the check must be signed (and if it isn't we
    // will check the check box ready to test the next rule)
    ensureSignedCheckBoxIsChecked();

    // Business rule #5: the Pay button should be enabled since all the
    // previous tests pass, the check is signed and now we have filled in
    // the account details
    populateCheckFields();
    var payButton = waitForObject(payButtonName);
    test.verify(payButton.isEnabled());
}
sub main
{
    startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/java/paymentform_swt/PaymentFormSWT.jar\"");
    # Import functionality needed by more than one test script
    source(findFile("scripts", "common.pl"));

    # Start with the correct tab
    clickTabItem("Chec&k");

    # Business rule #1: the minimum payment is $10 and the maximum is
    # $250 or the amount due whichever is smaller
    my $amount_due = getAmountDue();
    checkPaymentRange(10, $amount_due < 250 ? $amount_due : 250);

    # Business rule #2: the check date must be no earlier than 30 days
    # ago and no later than tomorrow, and must initially be set to
    # today. Here we just check its initial value.
    my $checkDateTime = waitForObject($Names::check_check_date_datetime);
    my $today = java_util_Calendar::getInstance();
    test::verify(dateTimeEqualsDate($checkDateTime, $today));

    # Business rule #3: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use waitForObjectExists()
    my $payButtonName = $Names::payment_form_pay_org_eclipse_swt_widgets_button;
    my $payButton = waitForObjectExists($payButtonName);
    test::verify(!$payButton->isEnabled());

    # Business rule #4: the check must be signed (and if it isn't we
    # will check the check box ready to test the next rule)
    ensureSignedCheckBoxIsChecked();

    # Business rule #5: the Pay button should be enabled since all the
    # previous tests pass, the check is signed and now we have filled in
    # the account details
    populateCheckFields();
    $payButton = waitForObject($payButtonName);
    test::verify($payButton->isEnabled());
}
def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/java/paymentform_swt/PaymentFormSWT.jar\"")
    # Import functionality needed by more than one test script
    require findFile("scripts", "common.rb")

    # Start with the correct tab
    clickTabItem("Chec&k")

    # Business rule #1: the minimum payment is $10 and the maximum is
    # $250 or the amount due whichever is smaller
    amount_due = getAmountDue
    checkPaymentRange(10, min(250, amount_due))

    # Business rule #2: the check date must be no earlier than 30 days
    # ago and no later than tomorrow, and must initially be set to
    # today. Here we just check its initial value.
    checkDateTime = waitForObject(Names::Check_Check_Date_DateTime)
    today = LC::Java_util_Calendar.getInstance()
    Test.verify(dateTimeEqualsDate(checkDateTime, today))

    # Business rule #3: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use waitForObjectExists()
    payButtonName = Names::Payment_Form_Pay_org_eclipse_swt_widgets_Button
    payButton = waitForObjectExists(payButtonName)
    Test.verify(!payButton.isEnabled())

    # Business rule #4: the check must be signed (and if it isn't we
    # will check the check box ready to test the next rule)
    ensureSignedCheckBoxIsChecked

    # Business rule #5: the Pay button should be enabled since all the
    # previous tests pass, the check is signed and now we have filled in
    # the account details
    populateCheckFields()
    payButton = waitForObject(payButtonName)
    Test.verify(payButton.isEnabled())
end
proc main {} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/java/paymentform_swt/PaymentFormSWT.jar\""
    # Import functionality needed by more than one test script
    source [findFile "scripts" "common.tcl"]

    # Start with the correct tab
    clickTabItem "Chec&k"

    # Business rule #1: the minimum payment is $10 and the maximum is
    # $250 or the amount due whichever is smaller
    set amount_due [getAmountDue]
    checkPaymentRange 10 [expr 250 > $amount_due ? $amount_due : 250]

    # Business rule #2: the check date must be no earlier than 30 days
    # ago and no later than tomorrow, and must initially be set to
    # today. Here we just check its initial value.
    set checkDateTime [waitForObject ":Check.Check Date:_DateTime"]
    set today [invoke java_util_Calendar getInstance]
    test verify [dateTimeEqualsDate $checkDateTime $today]

    # Business rule #3: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use waitForObjectExists()
    set payButtonName ":Payment Form.Pay_org.eclipse.swt.widgets.Button"
    set payButton [waitForObjectExists $payButtonName]
    test verify [expr ![invoke $payButton isEnabled]]

    # Business rule #4: the check must be signed (and if it isn't we
    # will check the check box ready to test the next rule)
    ensureSignedCheckBoxIsChecked

    # Business rule #5: the Pay button should be enabled since all the
    # previous tests pass, the check is signed and now we have filled in
    # the account details
    populateCheckFields
    set payButton [waitForObject $payButtonName]
    test verify [invoke $payButton isEnabled]
}

source(filename)函数(或Ruby中的require),用于读取脚本并执行它。通常这种脚本仅用于明确定义事物——例如,函数——然后这些函数成为测试脚本可用。

第一条业务规则与之前非常相似,只不过这次我们使用了共同的checkPaymentRange函数。第二条规则显示了如何使用常用的dateTimeEqualsDate函数测试DateTime对象的属性。

Squish可以自动提供对大多数Java API的访问,但在某些情况下,我们需要访问默认不可用的类。在这个测试中,我们需要一个默认不可用的类,即java.util.Calendar。实际上这并不成问题,因为我们总是可以与squishserver注册额外的类(无论是标准类还是我们自己的自定义类)——有关详细信息,请参阅封装自定义类。在这种情况下,我们在java.ini中添加了几个额外的类,该文件只有两行。

[general]
AutClasses="java.util.Calendar","java.util.Date",\
"java.text.DateFormat","java.text.SimpleDateFormat"

另一种注册额外类的方法是使用Java的自省功能创建它们的实例。

第三条规则检查“支付”按钮是否被禁用,因为在这个阶段表单的数据不是有效的。(例如,检查尚未签署,且没有填写账户详细信息。)第四条规则用于确保和确认检查已被签署——我们将整个功能都封装在ensureSignedCheckBoxIsChecked函数中。对于第五条规则,我们使用populateCheckFields函数用假数据填充账户行编辑。最后,所有的小部件都有有效的值,所以支付按钮应该是启用的,最后一行测试了这一点。

def ensureSignedCheckBoxIsChecked():
    checkSignedButton = waitForObject(names.check_Check_Signed_Button)
    if not checkSignedButton.getSelection():
        clickButton(checkSignedButton)
    snooze(1)
    test.verify(checkSignedButton.getSelection())

def populateCheckFields():
    bankNameText = waitForObject(names.check_Bank_Name_Text)
    type(bankNameText, "A Bank")
    bankNumberText = waitForObject(names.check_Bank_Number_Text)
    type(bankNumberText, "88-91-33X")
    accountNameText = waitForObject(names.check_Account_Name_Text)
    type(accountNameText, "An Account")
    accountNumberText = waitForObject(names.check_Account_Number_Text)
    type(accountNumberText, "932745395")
function ensureSignedCheckBoxIsChecked()
{
    var checkSignedButton = waitForObject(names.checkCheckSignedButton);
    if (!checkSignedButton.getSelection()) {
        clickButton(checkSignedButton);
    }
    snooze(1);
    test.verify(checkSignedButton.getSelection());
}

function populateCheckFields()
{
    var bankNameText = waitForObject(names.checkBankNameText);
    type(bankNameText, "A Bank");
    var bankNumberText = waitForObject(names.checkBankNumberText);
    type(bankNumberText, "88-91-33X");
    var accountNameText = waitForObject(names.checkAccountNameText);
    type(accountNameText, "An Account");
    var accountNumberText = waitForObject(names.checkAccountNumberText);
    type(accountNumberText, "932745395");
}
sub ensureSignedCheckBoxIsChecked
{
    my $checkSignedButton = waitForObject($Names::check_check_signed_button);
    if (!$checkSignedButton->getSelection()) {
        clickButton($checkSignedButton);
    }
    test::verify($checkSignedButton->getSelection());
}

sub populateCheckFields
{
    my $bankNameText = waitForObject($Names::check_bank_name_text);
    type($bankNameText, "A Bank");
    my $bankNumberText = waitForObject($Names::check_bank_number_text);
    type($bankNumberText, "88-91-33X");
    my $accountNameText = waitForObject($Names::check_account_name_text);
    type($accountNameText, "An Account");
    my $accountNumberText = waitForObject($Names::check_account_number_text);
    type($accountNumberText, "932745395");
}
def ensureSignedCheckBoxIsChecked
    checkSignedButton = waitForObject(Names::Check_Check_Signed_Button)
    if !checkSignedButton.getSelection()
        clickButton(checkSignedButton)
    end
    snooze(1)
    Test.verify(checkSignedButton.getSelection())
end

def populateCheckFields
    bankNameText = waitForObject(Names::Check_Bank_Name_Text)
    type(bankNameText, "A Bank")
    bankNumberText = waitForObject(Names::Check_Bank_Number_Text)
    type(bankNumberText, "88-91-33X")
    accountNameText = waitForObject(Names::Check_Account_Name_Text)
    type(accountNameText, "An Account")
    accountNumberText = waitForObject(Names::Check_Account_Number_Text)
    type(accountNumberText, "932745395")
end
proc ensureSignedCheckBoxIsChecked {} {
    set checkSignedButton [waitForObject ":Check.Check Signed_Button"]
    if {![invoke $checkSignedButton getSelection]} {
        invoke clickButton $checkSignedButton
    }
    snooze 1
    test verify [invoke $checkSignedButton getSelection]
}

proc populateCheckFields {} {
    set bankNameText [waitForObject ":Check.Bank Name:_Text"]
    invoke type $bankNameText "A Bank"
    set bankNumberText [waitForObject ":Check.Bank Number:_Text"]
    invoke type $bankNumberText "88-91-33X"
    set accountNameText [waitForObject ":Check.Account Name:_Text"]
    invoke type $accountNameText "An Account"
    set accountNumberText [waitForObject ":Check.Account Number:_Text"]
    invoke type $accountNumberText "932745395"
}

请注意,我们使用了type(objectOrName, text)函数来模拟用户输入文本。通常直接设置小部件属性不如模拟用户交互——毕竟,我们通常需要测试的是用户体验到的应用程序的行为。

我们现在准备查看表单业务逻辑的最后一个测试——“卡片”模式的测试。与“检查”模式一样,我们将main函数重构,以使其简短易懂,依赖于在test.py(或test.js等)文件中的通用函数和针对特定测试的函数来封装特定的测试。

def main():
    startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/java/paymentform_swt/PaymentFormSWT.jar"')
    source(findFile("scripts", "common.py"))

    # Start with the correct tab
    clickTabItem("&Card")

    # Business rule #1: the minimum payment is $10 or 5% of the amount due
    # whichever is larger and the maximum is $5000 or the amount due
    # whichever is smaller
    amount_due = getAmountDue()
    checkPaymentRange(max(10, amount_due / 20.0), min(5000, amount_due))

    # Make sure the card type is not Master, i.e., not the default, just
    # to show how to manipulate a Combo.
    cardTypeCombo = waitForObject(names.card_Card_Type_Combo)
    for index in range(cardTypeCombo.getItemCount()):
        if cardTypeCombo.getItem(index) != "Master":
            cardTypeCombo.select(index)
            break

    # Business rules #2 and 3: the issue date must be no
    # earlier than 3 years ago, and must start out at that date, and the
    # expiry date must be at least a month later than today.
    # All we check here is that both DateTimes are set to their earliest
    # valid date.
    checkCardDateDetails()

    # Business rule #4: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use waitForObjectExists()
    payButtonName = names.payment_Form_Pay_org_eclipse_swt_widgets_Button
    payButton = waitForObjectExists(payButtonName)
    test.verify(not payButton.isEnabled())

    # Business rule #5: the Pay button should be enabled since all the
    # previous tests pass, and now we have filled in the account details
    populateCardFields()
    payButton = waitForObject(payButtonName)
    test.verify(payButton.isEnabled())
function main()
{
    startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/java/paymentform_swt/PaymentFormSWT.jar"');
    source(findFile("scripts", "common.js"));

    // Start with the correct tab
    clickTabItem("&Card");

    // Business rule #1: the minimum payment is $10 or 5% of the
    // amount due whichever is larger and the maximum is $5000 or the
    // amount due whichever is smaller
    var amount_due = getAmountDue();
    checkPaymentRange(Math.max(10, amount_due / 20.0),
                      Math.min(5000, amount_due));

    // Make sure the card type is not Master, i.e., not the default,
    // just to show how to manipulate a Combo.
    var cardTypeCombo = waitForObject(names.cardCardTypeCombo);
    for (var index = 0; index < cardTypeCombo.getItemCount(); ++index) {
        if (cardTypeCombo.getItem(index) != "Master") {
            cardTypeCombo.select(index);
            break;
        }
    }

    // Business rules #2 and #3: the issue date must be no
    // earlier than 3 years ago, and must start out at that date, and the
    // expiry date must be at least a month later than today.
    // All we check here is that both DateTimes are set to their earliest
    // valid date.
    checkCardDateDetails();

    // Business rule #4: the Pay button is disabled (since the form's data
    // isn't yet valid), so we use waitForObjectExists()
    var payButtonName = names.paymentFormPayOrgEclipseSwtWidgetsButton;
    var payButton = waitForObjectExists(payButtonName);
    test.verify(!payButton.isEnabled());

    // Business rule #5: the Pay button should be enabled since all the
    // previous tests pass, and now we have filled in the account details
    populateCardFields();
    var payButton = waitForObject(payButtonName);
    test.verify(payButton.isEnabled());
}
sub main
{
    startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/java/paymentform_swt/PaymentFormSWT.jar\"");
    source(findFile("scripts", "common.pl"));

    # Start with the correct tab
    clickTabItem("&Card");

    # Business rule #1: the minimum payment is $10 or 5% of the amount due
    # whichever is larger and the maximum is $5000 or the amount due
    # whichever is smaller
    my $amount_due = getAmountDue();
    checkPaymentRange($amount_due / 20.0 > 10 ? $amount_due / 20.0 : 10,
                      $amount_due < 5000 ? $amount_due : 5000);

    # Make sure the card type is not Master, i.e., not the default, just
    # to show how to manipulate a Combo.
    my $cardTypeCombo = waitForObject($Names::card_card_type_combo);
    for (my $index = 0; $index < $cardTypeCombo->getItemCount(); ++$index) {
        if ($cardTypeCombo->getItem($index) ne "Master") {
            $cardTypeCombo->select($index);
            last;
        }
    }

    # Business rules #2 and 3: the issue date must be no
    # earlier than 3 years ago, and must start out at that date, and the
    # expiry date must be at least a month later than today.
    # All we check here is that both DateTimes are set to their earliest
    # valid date.
    checkCardDateDetails();

    # Business rule #4: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use waitForObjectExists()
    my $payButtonName = $Names::payment_form_pay_org_eclipse_swt_widgets_button;
    my $payButton = waitForObjectExists($payButtonName);
    test::verify(!$payButton->isEnabled());

    # Business rule #5: the Pay button should be enabled since all the
    # previous tests pass, and now we have filled in the account details
    populateCardFields();
    $payButton = waitForObject($payButtonName);
    test::verify($payButton->isEnabled());
}
def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/java/paymentform_swt/PaymentFormSWT.jar\"")
    require findFile("scripts", "common.rb")

    # Start with the correct tab
    clickTabItem("&Card")

    # Business rule #1: the minimum payment is $10 or 5% of the amount due
    # whichever is larger and the maximum is $5000 or the amount due
    # whichever is smaller
    amount_due = getAmountDue()
    checkPaymentRange(max(10, amount_due / 20.0), min(5000, amount_due))

    # Make sure the card type is not Master, i.e., not the default, just
    # to show how to manipulate a Combo.
    cardTypeCombo = waitForObject(Names::Card_Card_Type_Combo)
    for index in 0...cardTypeCombo.getItemCount()
        if cardTypeCombo.getItem(index) != "Master"
            cardTypeCombo.select(index)
            break
        end
    end

    # Business rules #2 and 3: the issue date must be no
    # earlier than 3 years ago, and must start out at that date, and the
    # expiry date must be at least a month later than today.
    # All we check here is that both DateTimes are set to their earliest
    # valid date.
    checkCardDateDetails

    # Business rule #4: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use waitForObjectExists()
    payButtonName = Names::Payment_Form_Pay_org_eclipse_swt_widgets_Button
    payButton = waitForObjectExists(payButtonName)
    Test.verify(!payButton.isEnabled())

    # Business rule #5: the Pay button should be enabled since all the
    # previous tests pass, and now we have filled in the account details
    populateCardFields
    payButton = waitForObject(payButtonName)
    Test.verify(payButton.isEnabled())
end
proc main {} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/java/paymentform_swt/PaymentFormSWT.jar\""
    source [findFile "scripts" "common.tcl"]

    # Start with the correct tab
    clickTabItem "&Card"

    # Business rule #1: the minimum payment is $10 or 5% of the amount
    # due whichever is larger and the maximum is $5000 or the amount
    # due whichever is smaller
    set amount_due [getAmountDue]
    set five_percent [expr $amount_due / 20.0]
    set minimum [expr 10 > $five_percent ? 10 : $five_percent]
    set maximum [expr 5000 > $amount_due ? $amount_due : 5000]
    checkPaymentRange $minimum $maximum

    # Make sure the card type is not Master, i.e., not the default, just
    # to show how to manipulate a Combo.
    set cardTypeCombo [waitForObject ":Card.Card Type:_Combo"]
    set count [invoke $cardTypeCombo getItemCount]
    for {set index 0} {$index < $count} {incr index} {
        if {[invoke $cardTypeCombo getItem $index] != "Master"} {
            invoke $cardTypeCombo select $index
            break
        }
    }

    # Business rules #2 and 3: the issue date must be no
    # earlier than 3 years ago, and must start out at that date, and the
    # expiry date must be at least a month later than today.
    # All we check here is that both DateTimes are set to their earliest
    # valid date.
    checkCardDateDetails

    # Business rule #4: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use waitForObjectExists()
    set payButtonName ":Payment Form.Pay_org.eclipse.swt.widgets.Button"
    set payButton [waitForObjectExists $payButtonName]
    test verify [expr ![invoke $payButton isEnabled]]

    # Business rule #5: the Pay button should be enabled since all the
    # previous tests pass, and now we have filled in the account details
    populateCardFields
    set payButton [waitForObject $payButtonName]
    test verify [invoke $payButton isEnabled]
}

我们首先设置正确的选项卡,然后对于第一条业务规则,我们检查支付范围——这与其他模式一样。

虽然测试业务规则不需要这么做,但我们更改了ComboBox小部件中的项目,只是为了展示这样做的方法。

第二条和第三条业务规则比较复杂,所以我们把它们封装在针对特定测试的checkCardDateDetails函数中。

最初,Pay按钮应该是禁用的,所以业务规则四会检查这一点。对于第五条业务规则,我们使用populateCardFields函数为卡片账户名称和号码提供一些假数据,并且由于日期在范围内,所以根据我们的最后检查,Pay按钮现在应该被启用。

def checkCardDateDetails():
    tabFolderName = names.payment_Form_org_eclipse_swt_widgets_TabFolder
    tabFolder = waitForObject(tabFolderName)
    for index in range(tabFolder.getItemCount()):
        tabItem = tabFolder.getItem(index)
        if tabItem.getText() == "&Card":
            break
    dateTimes = []
    control = tabItem.getControl()
    children = control.getChildren()
    for index in range(children.length):
        child = children.at(index)
        if (child.getClass().toString() ==
            "class org.eclipse.swt.widgets.DateTime"):
            dateTimes.append(child)
    test.verify(len(dateTimes) == 2)

    earliestIssueDate = java_util_Calendar.getInstance()
    earliestIssueDate.add(java_util_Calendar.YEAR, -3)
    test.verify(dateTimeEqualsDate(dateTimes[0], earliestIssueDate))
    earliestExpiryDate = java_util_Calendar.getInstance()
    earliestExpiryDate.add(java_util_Calendar.MONTH, 1)
    test.verify(dateTimeEqualsDate(dateTimes[1], earliestExpiryDate))

def populateCardFields():
    cardAccountNameText = waitForObject(names.card_Account_Name_Text)
    mouseClick(cardAccountNameText, 10, 10, 0, Button.Button1)
    type(cardAccountNameText, "An Account")
    cardAccountNumberText = waitForObject(names.card_Account_Number_Text)
    type(cardAccountNumberText, "1343 876 326 1323 32")
function checkCardDateDetails()
{
    var tabFolderName = names.paymentFormOrgEclipseSwtWidgetsTabFolder;
    var tabFolder = waitForObject(tabFolderName);
    for (var index = 0; index < tabFolder.getItemCount(); ++index) {
        var tabItem = tabFolder.getItem(index);
        if (tabItem.getText() == "&Card") {
            break;
        }
    }
    var dateTimes = [];
    var control = tabItem.getControl();
    var children = control.getChildren();
    for (var index = 0; index <  children.length; ++index) {
        var child = children.at(index);
        if (child.getClass().toString() ==
            "class org.eclipse.swt.widgets.DateTime") {
            dateTimes.push(child);
        }
    }
    test.verify(dateTimes.length == 2);

    var earliestIssueDate = java_util_Calendar.getInstance();
    earliestIssueDate.add(java_util_Calendar.YEAR, -3);
    test.verify(dateTimeEqualsDate(dateTimes[0], earliestIssueDate));
    var earliestExpiryDate = java_util_Calendar.getInstance();
    earliestExpiryDate.add(java_util_Calendar.MONTH, 1);
    test.verify(dateTimeEqualsDate(dateTimes[1], earliestExpiryDate));
}

function populateCardFields()
{
    var cardAccountNameText = waitForObject(names.cardAccountNameText);
    mouseClick(cardAccountNameText, 10, 10, 0, Button.Button1);
    type(cardAccountNameText, "An Account");
    var cardAccountNumberText = waitForObject(names.cardAccountNumberText);
    type(cardAccountNumberText, "1343 876 326 1323 32");
}
sub checkCardDateDetails
{
    my $tabFolderName = $Names::payment_form_org_eclipse_swt_widgets_tabfolder;
    my $tabFolder = waitForObject($tabFolderName);
    my $tabItem;
    for (my $index = 0; $index < $tabFolder->getItemCount(); ++$index) {
        $tabItem = $tabFolder->getItem($index);
        if ($tabItem->getText() eq "&Card") {
            last;
        }
    }
    my @dateTimes = ();
    my $control = $tabItem->getControl();
    my $children = $control->getChildren();
    for (my $index = 0; $index < $children->length(); ++$index) {
        my $child = $children->at($index);
        if ($child->getClass()->toString() eq
            "class org.eclipse.swt.widgets.DateTime") {
            push @dateTimes, $child;
        }
    }
    test::verify(@dateTimes == 2);

    my $earliestIssueDate = java_util_Calendar::getInstance();
    $earliestIssueDate->add(java_util_Calendar->YEAR, -3);
    test::verify(dateTimeEqualsDate($dateTimes[0], $earliestIssueDate));
    my $earliestExpiryDate = java_util_Calendar::getInstance();
    $earliestExpiryDate->add(java_util_Calendar->MONTH, 1);
    test::verify(dateTimeEqualsDate($dateTimes[1], $earliestExpiryDate));
}

sub populateCardFields
{
    my $cardAccountNameText = waitForObject($Names::card_account_name_text);
    mouseClick($cardAccountNameText, 10, 10, 0, Button::Button1);
    type($cardAccountNameText, "An Account");
    my $cardAccountNumberText = waitForObject(
        $Names::card_account_number_text);
    type($cardAccountNumberText, "1343 876 326 1323 32");
}
def checkCardDateDetails
    tabFolderName = Names::Payment_Form_org_eclipse_swt_widgets_TabFolder
    tabFolder = waitForObject(tabFolderName)
    for index in 0...tabFolder.getItemCount()
        tabItem = tabFolder.getItem(index)
        if tabItem.getText() == "&Card"
            break
        end
    end
    dateTimes = []
    control = tabItem.getControl()
    children = control.getChildren()
    for index in 0...children.length
        child = children.at(index)
        if (child.getClass().toString() ==
        "class org.eclipse.swt.widgets.DateTime")
            dateTimes << child
        end
    end
    Test.verify(dateTimes.length == 2)

    earliestIssueDate = LC::Java_util_Calendar.getInstance()
    earliestIssueDate.add(LC::Java_util_Calendar.YEAR, -3)
    Test.verify(dateTimeEqualsDate(dateTimes[0], earliestIssueDate))
    earliestExpiryDate = LC::Java_util_Calendar.getInstance()
    earliestExpiryDate.add(LC::Java_util_Calendar.MONTH, 1)
    Test.verify(dateTimeEqualsDate(dateTimes[1], earliestExpiryDate))
end

def populateCardFields
    cardAccountNameText = waitForObject(Names::Card_Account_Name_Text)
    mouseClick(cardAccountNameText, 10, 10, 0, Button::BUTTON1)
    type(cardAccountNameText, "An Account")
    cardAccountNumberText = waitForObject(Names::Card_Account_Number_Text)
    type(cardAccountNumberText, "1343 876 326 1323 32")
end
proc checkCardDateDetails {} {
    set tabFolderName ":Payment Form_org.eclipse.swt.widgets.TabFolder"
    set tabFolder [waitForObject $tabFolderName]
    set count [invoke $tabFolder getItemCount]
    for {set index 0} {$index < $count} {incr index} {
        set tabItem [invoke $tabFolder getItem $index]
        if {[invoke $tabItem getText] == "&Card"} {
            break
        }
    }
    set dateTimes {}
    set control [invoke $tabItem getControl]
    set children [invoke $control getChildren]
    set count [property get $children length]
    for {set index 0} {$index < $count} {incr index} {
        set child [invoke $children at $index]
        set className [invoke [invoke $child getClass] toString]
        if {$className == "class org.eclipse.swt.widgets.DateTime"} {
            lappend dateTimes $child
        }
    }
    test compare [llength $dateTimes] 2

    set earliestIssueDate [invoke java_util_Calendar getInstance]
    invoke $earliestIssueDate add [property get java_util_Calendar YEAR] -3
    set issueDateTime [lindex $dateTimes 0]
    test verify [dateTimeEqualsDate $issueDateTime $earliestIssueDate]
    set earliestExpiryDate [invoke java_util_Calendar getInstance]
    invoke $earliestExpiryDate add [property get java_util_Calendar \
        MONTH] 1
    set expiryDateTime [lindex $dateTimes 1]
    test verify [dateTimeEqualsDate $expiryDateTime $earliestExpiryDate]
}

proc populateCardFields {} {
    set cardAccountNameText [waitForObject ":Card.Account Name:_Text"]
    invoke mouseClick $cardAccountNameText 10 10 0 [enum Button Button1]
    invoke type $cardAccountNameText "An Account"
    set cardAccountNumberText [waitForObject \
        ":Card.Account Number:_Text"]
    invoke type $cardAccountNumberText "1343 876 326 1323 32"
}

checkCardDateDetails函数中,对于发行日期和到期日期的DateTime小部件,我们想检查它们都设置为它们的最小有效日期。而不是按名称识别它们,我们使用了自省来找到它们。我们首先获取对TabFolder的引用。然后我们使用选项卡的API遍历每个TabItem,直到我们得到我们想要的那个。然后我们获取小部件数组的子部件——这样的数组具有length属性和at方法,我们使用这些来遍历每个子项,并保留两个DateTime小部件的引用。

一旦我们有了日期时间小部件,我们检查恰好有两个,并且它们都被设置为预期的日期。

对于populateCardFields函数,我们使用type(objectOrName, text)函数输入虚构数据。

我们已经完成了使用状态和单值小部件测试业务规则的评审。Java/SWT有许多其他类似的小部件,但它们都使用与我们在这里使用相同的技术进行识别和测试。

如何测试列表、表和树小部件(Java/SWT)

在本节中,我们将了解如何遍历Java SWT的列表、表和树小部件中的每个项目,以及如何获取每个项目的信息,例如它们的文本和选中、勾选状态。

尽管示例仅将每个项目的文本和选中、勾选状态输出到Squish的日志中,但它们很容易适应进行更复杂的测试,例如比较实际值与预期值。

本节中显示的所有代码都是从examples/java/itemviews_swt示例测试套件中取出的。

如何测试列表

如以下测试示例所示,遍历列表中的所有项目并检索它们的文本和选中状态非常容易

import os
def main():
    startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/java/itemviews_swt/ItemViewsSWT.jar"')
    listWidgetName = ":Item Views_org.eclipse.swt.widgets.List"
    listWidget = waitForObject(listWidgetName)
    for row in range(listWidget.getItemCount()):
        text = listWidget.getItem(row)
        selected = ""
        if listWidget.isSelected(row):
            selected = " +selected"
        test.log("(%d) '%s'%s" % (row, text, selected))
function main()
{
    startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/java/itemviews_swt/ItemViewsSWT.jar"');
    var listWidgetName = ":Item Views_org.eclipse.swt.widgets.List";
    var listWidget = waitForObject(listWidgetName);
    for (var row = 0; row < listWidget.getItemCount(); ++row) {
        var text = listWidget.getItem(row);
        var selected = "";
        if (listWidget.isSelected(row)) {
            selected = " +selected";
        }
        test.log("(" + String(row) + ") '" + text + "'" + selected);
    }
}
sub main
{
    startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/java/itemviews_swt/ItemViewsSWT.jar\"");
    my $listWidgetName = ":Item Views_org.eclipse.swt.widgets.List";
    my $listWidget = waitForObject($listWidgetName);
    for (my $row = 0; $row < $listWidget->getItemCount(); ++$row) {
        my $text = $listWidget->getItem($row);
        my $selected = "";
        if ($listWidget->isSelected($row)) {
            $selected = " +selected";
        }
        test::log("($row) '$text'$selected");
    }
}
# encoding: UTF-8
require 'squish'

include Squish

def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/java/itemviews_swt/ItemViewsSWT.jar\"")
    listWidgetName = ":Item Views_org.eclipse.swt.widgets.List"
    listWidget = waitForObject(listWidgetName)
    for row in 0...listWidget.getItemCount()
        text = listWidget.getItem(row)
        selected = ""
        if listWidget.isSelected(row)
            selected = " +selected"
        end
        Test.log("(#{row}) '#{text}'#{selected}")
    end
end
proc main {} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/java/itemviews_swt/ItemViewsSWT.jar\""
    set listWidgetName ":Item Views_org.eclipse.swt.widgets.List"
    set listWidget [waitForObject $listWidgetName]
    for {set row 0} {$row < [invoke $listWidget getItemCount]} \
        {incr row} {
        set text [invoke $listWidget getItem $row]
        set selected ""
        if {[invoke $listWidget isSelected $row]} {
            set selected " +selected"
        }
        test log "($row) '$text'$selected"
    }
}

所有输出都输送到 Squish 的日志中,但显然很容易更改脚本以针对特定值进行测试等。

如何测试表

如以下测试示例所示,遍历表中的所有项目并检索它们的文本和勾选、选中状态也很容易

import os
def main():
    startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/java/itemviews_swt/ItemViewsSWT.jar"')
    tableWidgetName = ":Item Views_org.eclipse.swt.widgets.Table"
    tableWidget = waitForObject(tableWidgetName)
    for row in range(tableWidget.getItemCount()):
        item = tableWidget.getItem(row)
        checked = ""
        if item.getChecked():
            checked = " +checked"
        selected = ""
        if tableWidget.isSelected(row):
            selected = " +selected"
        texts = []
        for column in range(tableWidget.getColumnCount()):
            texts.append(item.getText(column))
        test.log("(%d, 0) '%s'%s%s" % (row, "|".join(texts), checked,
            selected))
function main()
{
    startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/java/itemviews_swt/ItemViewsSWT.jar"');
    var tableWidgetName = ":Item Views_org.eclipse.swt.widgets.Table";
    var tableWidget = waitForObject(tableWidgetName);
    for (var row = 0; row < tableWidget.getItemCount(); ++row) {
        var item = tableWidget.getItem(row);
        var checked = "";
        if (item.getChecked()) {
            checked = " +checked";
        }
        var selected = "";
        if (tableWidget.isSelected(row)) {
            selected = " +selected";
        }
        var text = "";
        for (var column = 0; column < tableWidget.getColumnCount();
            ++column) {
            if (text != "") {
                text += "|";
            }
            text += item.getText(column);
        }
        test.log("(" + String(row) + ", 0) '" + text + "'" + checked +
            selected);
    }
}
sub main
{
    startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/java/itemviews_swt/ItemViewsSWT.jar\"");
    my $tableWidgetName = ":Item Views_org.eclipse.swt.widgets.Table";
    my $tableWidget = waitForObject($tableWidgetName);
    for (my $row = 0; $row < $tableWidget->getItemCount(); ++$row) {
        my $item = $tableWidget->getItem($row);
        my $checked = "";
        if ($item->getChecked()) {
            $checked = " +checked";
        }
        my $selected = "";
        if ($tableWidget->isSelected($row)) {
            $selected = " +selected";
        }
        my @texts = ();
        for (my $column = 0; $column < $tableWidget->getColumnCount();
            ++$column) {
            push @texts, $item->getText($column);
        }
        $text = join("|", @texts);
        test::log("($row, 0) '$text'$checked$selected");
    }
}
# encoding: UTF-8
require 'squish'
include Squish

def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/java/itemviews_swt/ItemViewsSWT.jar\"")
    tableWidgetName = ":Item Views_org.eclipse.swt.widgets.Table"
    tableWidget = waitForObject(tableWidgetName)
    for row in 0...tableWidget.getItemCount()
        item = tableWidget.getItem(row)
        checked = ""
        if item.getChecked()
            checked = " +checked"
        end
        selected = ""
        if tableWidget.isSelected(row)
            selected = " +selected"
        end
        texts = []
        for column in 0...tableWidget.getColumnCount()
            texts << item.getText(column)
        end
        Test.log("(#{row}, 0) '%s'#{checked}#{selected}" % texts.join("|"))
    end
end
proc main {} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/java/itemviews_swt/ItemViewsSWT.jar\""
    set tableWidgetName ":Item Views_org.eclipse.swt.widgets.Table"
    set tableWidget [waitForObject $tableWidgetName]
    for {set row 0} {$row < [invoke $tableWidget getItemCount]} \
        {incr row} {
        set item [invoke $tableWidget getItem $row]
        set checked ""
        if {[invoke $item getChecked]} {
            set checked " +checked"
        }
        set selected ""
        if {[invoke $tableWidget isSelected $row]} {
            set selected " +selected"
        }
        set text {}
        for {set column 0} {$column < [invoke $tableWidget getColumnCount]} \
            {incr column} {
            lappend text [invoke $item getText $column]
        }
        set text [join $text "|"]
        test log "($row, 0) '$text'$checked$selected"
    }
}

在本例中,我们只在每个TableItem的第一列中放置文本,这就是为什么我们在输出中硬编码列0。尽管如此,代码展示了如何访问所有列中的文本,因此代码应该很容易调整。

再次,所有输出都输送到 Squish 的日志中,显然很容易更改脚本以针对特定值进行测试等。

注意:您可以使用表格验证点来检查整个表格,就如在 如何创建和使用表格验证 中所述。

如何测试树

遍历树中的所有项目并检索它们的文本和勾选、选中状态要复杂一些——因为树是一种递归结构。尽管如此,这是完全可能的,如下测试示例所示

    import os
    def checkAnItem(indent, item, selection):
        if indent > -1:
            checked = selected = ""
            if item.getChecked():
                checked = " +checked"
            for i in range(selection.length):
                if selection.at(i) == item:
                    selected = " +selected"
                    break
            test.log("|%s'%s'%s%s" % (" " * indent, item.getText(), checked,
                selected))
        else:
            indent = -4
        for row in range(item.getItemCount()):
            child = item.getItem(row)
            checkAnItem(indent + 4, child, selection)

def main():
        startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/java/itemviews_swt/ItemViewsSWT.jar"')
        treeWidgetName = ":Item Views_org.eclipse.swt.widgets.Tree"
        treeWidget = waitForObject(treeWidgetName)
        checkAnItem(-1, treeWidget, treeWidget.getSelection())
    function checkAnItem(indent, item, selection)
    {
        if (indent > -1) {
            var checked = "";
            var selected = "";
            if (item.getChecked()) {
                checked = " +checked";
            }
            for (var i = 0; i < selection.length; ++i) {
                if (selection.at(i) == item) {
                    selected = " +selected";
                    break;
                }
            }
            var offset = "";
            for (var i = 0; i < indent; ++i) {
                offset = offset.concat(" ");
            }
            test.log("|" + offset + "'" + item.getText() + "'" + checked +
                selected);
        }
        else {
            indent = -4;
        }
        for (var row = 0; row < item.getItemCount(); ++row) {
            var child = item.getItem(row);
            checkAnItem(indent + 4, child, selection);
        }
    }

function main()
    {
        startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/java/itemviews_swt/ItemViewsSWT.jar"');
        var treeWidgetName = ":Item Views_org.eclipse.swt.widgets.Tree";
        var treeWidget = waitForObject(treeWidgetName);
        checkAnItem(-1, treeWidget, treeWidget.getSelection());
    }
sub checkAnItem
{
    my ($indent, $item, $selection) = @_;
    if ($indent > -1) {
        my $checked = "";
        my $selected = "";
        if ($item->getChecked()) {
            $checked = " +checked";
        }
        my $padding = " " x $indent;
        for (my $i = 0; $i < $selection->length; ++$i) {
            if ($selection->at($i) eq $item) {
                $selected = " +selected";
                last;
            }
        }
        test::log("|$padding'" . $item->getText() . "'$checked$selected");
    }
    else {
        $indent = -4;
    }
    for (my $row = 0; $row < $item->getItemCount(); ++$row) {
        my $child = $item->getItem($row);
        checkAnItem($indent + 4, $child, $selection);
    }
}

sub main()
{
    startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/java/itemviews_swt/ItemViewsSWT.jar\"");
    my $treeWidgetName = ":Item Views_org.eclipse.swt.widgets.Tree";
    my $treeWidget = waitForObject($treeWidgetName);
    checkAnItem(-1, $treeWidget, $treeWidget->getSelection());
}
# encoding: UTF-8
require 'squish'
include Squish

def checkAnItem(indent, item, selection)
    if indent > -1
        checked = selected = ""
        if item.getChecked()
            checked = " +checked"
        end
        for i in 0...selection.length
            if selection.at(i) == item
                selected = " +selected"
                break
            end
        end
        Test.log("|%s'%s'%s%s" % [" " * indent, item.getText(), checked,
            selected])
    else
        indent = -4
    end
    for row in 0...item.getItemCount()
        child = item.getItem(row)
        checkAnItem(indent + 4, child, selection)
    end
end

def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/java/itemviews_swt/ItemViewsSWT.jar\"")
    treeWidgetName = ":Item Views_org.eclipse.swt.widgets.Tree"
    treeWidget = waitForObject(treeWidgetName)
    checkAnItem(-1, treeWidget, treeWidget.getSelection())
end
    proc checkAnItem {indent item selection} {
        if {$indent > -1} {
            set checked ""
            set selected ""
            if {[invoke $item getChecked]} {
                set checked " +checked"
            }
            set offset [string repeat " " $indent]
            for {set i 0} {$i < [property get $selection length]} {incr i} {
                if {[invoke [invoke $selection at $i] toString] == \
                    [invoke $item toString]} {
                    set selected " +selected"
                    break
                }
            }
            set text [invoke $item getText]
            test log "|$offset'$text'$checked$selected"
        } else {
            set indent -4
        }
        for {set row 0} {$row < [invoke $item getItemCount]} {incr row} {
            set child [invoke $item getItem $row]
            set offset [expr $indent + 4]
            checkAnItem $offset $child $selection
        }
    }

proc main {} {
        startApplication "\"$::env(SQUISH_PREFIX)/examples/java/itemviews_swt/ItemViewsSWT.jar\""
        set treeWidgetName ":Item Views_org.eclipse.swt.widgets.Tree"
        set treeWidget [waitForObject $treeWidgetName]
        checkAnItem -1 $treeWidget [invoke $treeWidget getSelection]
    }

与列表和表的主要区别在于,由于树是递归的,因此我们亲自使用递归遍历所有项目是最容易的。不幸的是,Java/SWT树API不允许我们询问特定项目是否已选中,因此我们必须对Tree.getSelection方法返回的选中项数组进行迭代。为此,我们将数组传递到checkAnItem函数的selection参数中。然后我们使用数组的length属性来查看我们可以遍历多少个项目,并使用其at方法依次检索每个项目。如果我们找到匹配的项目,我们知道它是选中的,因此我们将此信息添加到我们打印的文本中。(另见,如何创建和访问数组。)

与之前的示例一样,所有输出都进入Squish的日志,尽管很容易调整脚本以执行其他测试。

如何测试表小部件和使用外部数据文件(Java/SWT)

在本节中,我们将了解如何测试下面的CsvTableSWT.java程序。此程序使用表来显示.csv(逗号分隔值)文件的内容,并为操纵数据提供一些基本功能——插入和删除行以及交换列。在我们审查测试的过程中,我们将学习如何导入测试数据、操纵数据以及将表显示的内容与我们期望的内容进行比较。由于CSV表程序是主窗口风格的程序,我们还将学习如何测试菜单选项能否按照预期执行。

{}

CSV Table程序。

本例的源代码位于目录 SQUISHDIR/examples/java/csvtable_swt 中,测试套件位于其子目录下。例如,测试的Python版本位于目录 SQUISHDIR/examples/java/csvtable_swt/suite_py 中,测试的JavaScript版本位于 SQUISHDIR/examples/java/csvtable_swt/suite_js 中,依此类推。

我们将关注的第一个测试看似简单,仅包含几个语句。这种简单性是通过将几乎所有功能放入一个共享脚本中实现的,以避免代码重复。以下是代码

import os

def main():
    startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/java/csvtable_swt/CsvTableSWT.jar"')
    source(findFile("scripts", "common.py"))
    filename = "before.csv"
    doFileOpen(filename, "")
    table = waitForObject("{isvisible='true' "
        "type='org.eclipse.swt.widgets.Table'}")
    compareTableWithDataFile(table, filename)
function main()
{
    startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/java/csvtable_swt/CsvTableSWT.jar"');
    source(findFile("scripts", "common.js"));
    var filename = "before.csv";
    doFileOpen(filename, "");
    var table = waitForObject("{isvisible='true' " +
        "type='org.eclipse.swt.widgets.Table'}");
    compareTableWithDataFile(table, filename);
}
sub main
{
    startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/java/csvtable_swt/CsvTableSWT.jar\"");
    source(findFile("scripts", "common.pl"));
    my $filename = "before.csv";
    doFileOpen($filename);
    my $table = waitForObject("{isvisible='true' " .
        "type='org.eclipse.swt.widgets.Table'}");
    compareTableWithDataFile($table, $filename);
}
# encoding: UTF-8
require 'squish'
include Squish

def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/java/csvtable_swt/CsvTableSWT.jar\"")
    require findFile("scripts", "common.rb")
    filename = "before.csv"
    doFileOpen(filename, "")
    table = waitForObject("{isvisible='true' " +
    "type='org.eclipse.swt.widgets.Table'}")
    compareTableWithDataFile(table, filename)
end
proc main {} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/java/csvtable_swt/CsvTableSWT.jar\""
    source [findFile "scripts" "common.tcl"]
    set filename "before.csv"
    doFileOpen $filename ""
    set table [waitForObject "{isvisible='true' \
        type='org.eclipse.swt.widgets.Table'}"]
    compareTableWithDataFile $table $filename
}

我们首先启动应用程序——对于Java/SWT程序,这是一个设置正确环境和执行自动测试的shell脚本或 .bat 文件。一旦自动测试运行起来,我们接下来加载一个包含常用功能的脚本,就像我们在前面的部分中做的那样。然后我们调用一个自定义的 doFileOpen 函数,告诉程序打开指定的文件——这个过程将通过用户界面来实现,正如我们将在下面看到的。(第一个参数是要打开的文件名,第二个参数是当前文件的名称,在这个例子中是一个空字符串,因为我们是从零开始启动自动测试的。)接下来我们通过 Object waitForObject(objectOrName) 函数获取表格的引用,并最终检查表格的内容是否与测试套件测试数据中保存的数据文件的内容匹配。请注意,CSV表格程序和Squish都是使用它们完全独立的代码来加载和分析数据文件的。(有关如何将测试数据导入Squish的说明,请参阅 如何创建和使用共享数据和共享脚本。)

现在我们将探讨上述测试中使用的自定义函数。

def doFileOpen(fileToOpen, currentFile):
    invokeMenuItem("File", "Open...", currentFile)
    chooseFile(waitForObject(names.sWT), fileToOpen)

def invokeMenuItem(menu, item, filename):
    if filename:
        filename = " - " + filename
    menuName = {"caption": menu, "container": names.csv_Table_org_eclipse_swt_widgets_Menu, "type": "org.eclipse.swt.widgets.MenuItem"}
    waitForObject(menuName)
    activateItem(menuName)
    menuItemName = ("{caption='%s' type='org.eclipse.swt.widgets.MenuItem'}" % item)
    waitForObject(menuItemName)
    activateItem(menuItemName)

def compareTableWithDataFile(table, filename):
    for row, record in enumerate(testData.dataset(filename)):
        item = table.getItem(row)
        for column, name in enumerate(testData.fieldNames(record)):
            test.compare(testData.field(record, name), item.getText(column))
function doFileOpen(fileToOpen, currentFile)
{
    invokeMenuItem("File", "Open...", currentFile);
    chooseFile(waitForObject(names.swt), fileToOpen);
}

function invokeMenuItem(menu, item, filename)
{
    if (filename) {
        filename = " - " + filename;
    }
    var menuName = {"caption": menu, "container": names.csvTableOrgEclipseSwtWidgetsMenu, "type": "org.eclipse.swt.widgets.MenuItem"};
    waitForObject(menuName);
    activateItem(menuName);
    var menuItemName = "{caption='" + item + "'" +
            " type='org.eclipse.swt.widgets.MenuItem'}";
    waitForObject(menuItemName);
    activateItem(menuItemName);
}

function compareTableWithDataFile(table, filename)
{
    var records = testData.dataset(filename);
    for (var row = 0; row < records.length; ++row) {
        var item = table.getItem(row);
        columnNames = testData.fieldNames(records[row]);
        for (var column = 0; column < columnNames.length; ++column) {
            test.compare(testData.field(records[row], column),
                item.getText(column));
        }
    }
}
sub doFileOpen
{
    my ($fileToOpen, $currentFile) = @_;
    invokeMenuItem("File", "Open...", $currentFile);
    chooseFile(waitForObject($Names::swt), $fileToOpen);
}

sub invokeMenuItem
{
    my ($menu, $item, $filename) = @_;
    if ($filename) {
        $filename = " - " . $filename;
    }
    my $menuName = {"caption" => $menu, "container" => $Names::csv_table_org_eclipse_swt_widgets_menu, "type" => "org.eclipse.swt.widgets.MenuItem"};
    waitForObject($menuName);
    activateItem($menuName);
    my $menuItemName = {"caption"=>$item, "type"=>'org.eclipse.swt.widgets.MenuItem'};
    waitForObject($menuItemName);
    activateItem($menuItemName);
}

sub compareTableWithDataFile
{
    my ($table, $filename) = @_;
    my @records = testData::dataset($filename);
    for (my $row = 0; $row < scalar(@records); $row++) {
        my $item = $table->getItem($row);
        my @columnNames = testData::fieldNames($records[$row]);
        for (my $column = 0; $column < scalar(@columnNames); $column++) {
            test::compare(testData::field($records[$row], $column),
                $item->getText($column));
        }
    }
}
def doFileOpen(fileToOpen, currentFile)
    invokeMenuItem("File", "Open...", currentFile)
    chooseFile(waitForObject(Names::SWT), fileToOpen)
end

def invokeMenuItem(menu, item, filename)
    if filename and filename != ""
        filename = " - " + filename
    end
    menuName = {:caption => menu, :container => Names::CSV_Table_org_eclipse_swt_widgets_Menu, :type => "org.eclipse.swt.widgets.MenuItem"}
    waitForObject(menuName)
    activateItem(menuName)
    menuItemName = {:caption=> item, :type =>'org.eclipse.swt.widgets.MenuItem'}
    waitForObject(menuItemName)
    activateItem(menuItemName)
end

def compareTableWithDataFile(table, filename)
    TestData.dataset(filename).each_with_index do
        |record, row|
        item = table.getItem(row)
        for column in 0...TestData.fieldNames(record).length
            Test.compare(TestData.field(record, column), item.getText(column))
        end
    end
end
proc doFileOpen {fileToOpen currentFile} {
    invokeMenuItem "File" "Open..." $currentFile
    invoke chooseFile [waitForObject ":SWT"] $fileToOpen
}

proc invokeMenuItem {menu item filename} {
    if {$filename != ""} {
        set filename " - $filename"
    }
    set menuName "{caption='$menu' \
        container=':CSV Table${filename}_org.eclipse.swt.widgets.Menu' \
        type='org.eclipse.swt.widgets.MenuItem'}"
    waitForObject $menuName
    invoke activateItem $menuName
    set menuItemName "{caption='$item' \
        type='org.eclipse.swt.widgets.MenuItem'}"
    waitForObject $menuItemName
    invoke activateItem $menuItemName
}

proc compareTableWithDataFile {table filename} {
    set data [testData dataset $filename]
    for {set row 0} {$row < [llength $data]} {incr row} {
        set item [invoke $table getItem $row]
        set columnNames [testData fieldNames [lindex $data $row]]
        for {set column 0} {$column < [llength $columnNames]} \
            {incr column} {
            test compare [testData field [lindex $data $row] $column] \
                [invoke $item getText $column]
           }
    }
}

在用户界面中打开文件的 doFileOpen 函数开始时,使用我们在教程中首次看到的自定义 invokeMenuItem 函数。该文件对话框可能不是所有平台都相同,但Squish通过提供Java/SWT特定的 chooseFile(objectOrName, filename) 函数来通用化它。

invokeMenuItem 函数实际上模拟用户点击 Alt+k(其中 k 是一个字符,例如 "F" 表示文件菜单),然后是与所需操作相对应的字符(例如,"o" 表示 "Open")。

另一个方法是使用工具栏项。例如,CsvTableSWT示例有一个工具栏,包含“新建”、“打开”和“保存”的工具项,因此对于我们的自定义 doFileOpen 函数,我们可以将对我们自定义 invokeMenuItem 函数的调用替换为以下行

waitForObject(":Open_org.eclipse.swt.widgets.ToolItem");
mouseClick(":Open_org.eclipse.swt.widgets.ToolItem");

这些行等待打开工具项就绪,然后点击它。这些行将在JavaScript、Python、Ruby和Perl中生效,应该很容易看出如何通过将“打开”文本替换为我们感兴趣的任何工具项的文本来参数化它们。

文件打开后,程序应加载文件的数据。我们通过比较表格中显示的数据和文件中的数据来检查数据是否正确加载。此比较由自定义 compareTableWithDataFile 函数执行。此函数使用Squish的 Dataset testData.dataset(filename) 函数加载数据,以便可以通过Squish API访问。我们期望表中每个单元格的文本都与数据中的相应项匹配,并使用 Boolean test.compare(value1, value2) 函数检查这一点。

既然我们知道了如何比较表中的数据与文件中的数据,我们可以进行一些更雄心勃勃的测试。我们将加载 before.csv 文件,删除几行,在中间插入一行新数据,并在末尾追加一行新数据。然后我们将交换几对列。最后,数据应与 after.csv 文件匹配。

与其编写代码来完成所有这些操作,我们可以简单地记录一个测试脚本,打开文件并执行所有删除、插入和列交换操作。然后我们可以编辑已记录的测试脚本来在末尾添加几行代码,以比较实际结果与预期结果。下面是测试脚本的一部分,从手写代码的上一行开始,一直持续到脚本的末尾。

    source(findFile("scripts", "common.py"))
    table = waitForObject("{isvisible='true' "
        "type='org.eclipse.swt.widgets.Table'}")
    test.verify(table.getColumnCount() == 5)
    test.verify(table.getItemCount() == 12)
    compareTableWithDataFile(table, "after.csv")
    # End of Added by Hand
    waitForObject(names.file_org_eclipse_swt_widgets_MenuItem_2)
    activateItem(names.file_org_eclipse_swt_widgets_MenuItem_2)
    waitForObject(names.quit_org_eclipse_swt_widgets_MenuItem_2)
    activateItem(names.quit_org_eclipse_swt_widgets_MenuItem_2)
    waitForObject(names.sWT)
    closeMessageBox(names.sWT, SWT.NO)
    source(findFile("scripts", "common.js"));
    var table = waitForObject("{isvisible='true' " +
        "type='org.eclipse.swt.widgets.Table'}");
    test.verify(table.getColumnCount() == 5);
    test.verify(table.getItemCount() == 12);
    compareTableWithDataFile(table, "after.csv");
    // End of Added by Hand
    waitForObject(names.fileOrgEclipseSwtWidgetsMenuItem2);
    activateItem(names.fileOrgEclipseSwtWidgetsMenuItem2);
    waitForObject(names.quitOrgEclipseSwtWidgetsMenuItem);
    activateItem(names.quitOrgEclipseSwtWidgetsMenuItem);
    waitForObject(names.swt);
    closeMessageBox(names.swt, SWT.NO);
}
    source(findFile("scripts", "common.pl"));
    my $table = waitForObject("{isvisible='true' " .
        "type='org.eclipse.swt.widgets.Table'}");
    test::verify($table->getColumnCount() == 5);
    test::verify($table->getItemCount() == 12);
    compareTableWithDataFile($table, "after.csv");
    # End of Added by Hand
    waitForObject($Names::file_org_eclipse_swt_widgets_menuitem_2);
    activateItem($Names::file_org_eclipse_swt_widgets_menuitem_2);
    waitForObject($Names::quit_org_eclipse_swt_widgets_menuitem_2);
    activateItem($Names::quit_org_eclipse_swt_widgets_menuitem_2);
    waitForObject($Names::swt);
    closeMessageBox($Names::swt, SWT::NO);
}
    require findFile("scripts", "common.rb")
    table = waitForObject("{isvisible='true' " +
    "type='org.eclipse.swt.widgets.Table'}")
    Test.verify(table.getColumnCount() == 5)
    Test.verify(table.getItemCount() == 12)
    compareTableWithDataFile(table, "after.csv")
    # End of Added by Hand
    waitForObject(Names::File_org_eclipse_swt_widgets_MenuItem_2)
    activateItem(Names::File_org_eclipse_swt_widgets_MenuItem_2)
    waitForObject(Names::Quit_org_eclipse_swt_widgets_MenuItem_2)
    activateItem(Names::Quit_org_eclipse_swt_widgets_MenuItem_2)
    closeMessageBox(waitForObject(Names::SWT), SWT::NO)
end
    source [findFile "scripts" "common.tcl"]
    set table [waitForObject "{isvisible='true' \
        type='org.eclipse.swt.widgets.Table'}"]
    test compare [invoke $table getColumnCount] 5
    test compare [invoke $table getItemCount] 12
    compareTableWithDataFile $table "after.csv"
    # End of Added by Hand
    waitForObject ":File_org.eclipse.swt.widgets.MenuItem_2"
    invoke activateItem ":File_org.eclipse.swt.widgets.MenuItem_2"
    waitForObject ":Quit_org.eclipse.swt.widgets.MenuItem_2"
    invoke activateItem ":Quit_org.eclipse.swt.widgets.MenuItem_2"
    waitForObject ":SWT"
    invoke closeMessageBox ":SWT" [enum SWT NO]
}

注意,对于提供静态 open 方法(在用户完成操作时创建对话框并调用 dispose 函数,例如通过点击 OKCancel)的对话框很有必要注销 closeWindow() 调用,正如在 CsvTableSWT.java 中执行的那样。

根据摘录指示,添加的行不是插入到记录的测试脚本末尾,而是在程序终止之前——毕竟,我们需要程序在运行以查询其表。(行数不同的原因是每种脚本语言都记录了略微不同的交互。)

此示例展示了记录与手动编辑相结合的强大功能。如果将来程序中添加了新功能,我们可以以多种方式将其包含在测试中。最简单的方法是添加另一个测试脚本,记录,然后添加比较表与预期数据的所需行。另一种方法是在临时测试中记录对新特性的使用,然后将记录复制并粘贴到现有测试的合适位置,然后将文件更改为在末尾比较的文件,该文件考虑了原始数据的所有更改以及使用新功能产生的更改。或者,我们可以直接将测试片段记录到现有测试中。

如何使用代理测试列表和组合框

尽管Squish支持的不同的GUI Java工具包对列表和组合框有不同的实现,但它们的项目都被Squish包裹成ItemProxy对象。这些对象作为列表或组合框对象的子对象。子对象被称作item_0item_1等等。

ItemProxy对象有textselected属性。对于基于SWT的列表和组合框小部件,还有一个额外的属性叫做control。基于AWT/Swing的则有一个额外属性叫做component

{}

在间谍中,你会为每个项目得到一个衍生自ItemProxy的对象。

在SWT中,列表和组合框小部件有方法getSelectionIndex。Squish使用这个方法来生成selectionindex属性调用。因此对于SWT工具包,这两个测试是等效的

test.compare(list.selectionindex, 2);
test.verify(list.item_2.selected);

第二个测试如果列表少少于三项,会给出脚本错误。如果列表支持多项选择,第一个测试只能给出一个选择。在这种情况下,你可以只能使用生成的ItemProxy对象作为验证点。

如何测试GEF应用程序

Squish公开了GEF(图形编辑框架)库中的Figure和FigureCanvas。FigureCanvas的Figure层次结构公开了,并在Spy中可以检查。可以像这样测试Figure项目属性:

item = waitForObject(":First_FigureItem")
test.verify(item.visible);
test.verify(item.enabled);

如何使用GestureBuilder类

注意: 本节仅适用于在Windows 8或更高版本的JavaFX上运行的JavaFX。

该类的实例由readGesture(gesture-file)返回。然而,当记录的手势不适合窗口时,可以进行缩放和/或平移。

可能有用的是获取场景的大小。以下是一个在像素中使用Java脚本绑定获取这些大小的示例。

var fx_node = findObject(":A_JavaFX_Node");
var scene = fx_node.getScene();
var w = scene.getWidth();
var h = scene.getHeight();

假设手势是在4:3显示的全屏上记录的。当在宽屏幕上重播时,手势可能太大并且太多地位于左下角。然后一个Object GestureBuilder.scale(scaleX, scaleY, originX, originY)和一个Object GestureBuilder.translate(x, y)指向右上角的解决方案是可能的。

{}

旋转、缩放和平移变换的效果。

例如,将其按照3/4的大小缩放,向右50像素,向上10像素。

注意: 当使用squishide时,在脚本中断点使用控制台视图,以便在脚本中实验手势变换。

gesture(waitForObject(":some_object"), readGesture("Gesture_1").scale(0.75).translate(50,-10));

另一种方法可能是只在大右角进行缩放。

var gst = readGesture("Gesture_1");
gesture(waitForObject(":some_object"), gst.scale(0.75, 0.75, gst.areaWidth, 0));

在某些情况下,需要动态创建手势,例如,为了更精确的控制或依赖运行时状态信息。这时可以使用手势创建方法。

这里是一个抛物线手势的示例,两个手指在800x1200像素的屏幕上以顺时针方向一段时间内做曲线运动。

var tb = new GestureBuilder(800, 1280, GestureBuilder.Pixel);
tb.addStroke( 600, 400 );
tb.curveTo(1000, 500, 300, 300, 300, 200, 400 );
tb.addStroke( 200, 800 );
tb.curveTo(1000, 300, 900, 500, 900, 600, 800);
tb.build();
gesture(waitForObject(":some_object"), tb);

这里是一个放大手势的示例,两个手指向对方移动,同样是在一秒钟内完成。这次作为一条陈述来描述。

gesture(waitForObject(":some_object"),
        new GestureBuilder(800, 1280, GestureBuilder.Pixel)
           .addStroke( 500, 400 )
           .lineTo(1000, 700, 100 )
           .addStroke( 300, 700 )
           .lineTo(1000, 100, 1000)
           .build());

在上面的两个示例中,坐标值是基于800x1280像素的区域大小。对于不同场景大小或不同大小或位置的控件,需要一些计算来得到这些值。接下来,介绍一种可以帮助在处理这些操作时维持复杂度可控的策略。

  • 根据屏幕尺寸创建一个手势,在x轴[-0.5,0.5]和y轴[-0.5,0.5]的范围内,持续时间为1秒。
  • 将其移动到目标控件的中心。
  • 以控件尺寸的最大值缩放,以该控件的中心作为起点。
  • 调整持续时间。

{}

这里列出了一个S形图形的例子。

var fx_node = findObject(":A_JavaFX_Node");
var scene = fx_node.getScene();

var tb = new GestureBuilder(scene.width, scene.height, GestureBuilder.Pixel)
             .addStroke(0, 0.5)
             .curveTo(500, -0.5, 0.5, -0.5, 0, 0, 0)
             .curveTo(500, 0.5, 0, 0.5, -0.5, 0, -0.5)
             .build();

var widget = findObject(":Some widget");
var scale = widget.width > widget.height ? widget.height : widget.width;
var centerX = widget.screenX + widget.width/2;
var centerY = widget.screenY + widget.height/2;
gesture(widget,
        tb.translate(centerX, centerY)
          .scale(scale, -scale, centerX, centerY)
          .accelerate(1/2))

注意,这个示例将图形定义为以正Y轴向上。为了避免图形颠倒,需要在X轴上进行镜像。秘诀在于在垂直方向使用负的缩放因子。

为了保持定义的手势在-0.5到0.5的边界内,有一个优点是总大小为1。因此,它可以与控件大小一起缩放,而不会超出屏幕边界。具有(0, 0)中心的点让平移变得简单,只需将其移至控件中心。

©2024 The Qt Company Ltd. 包含在内的文档贡献者是各自所有者的版权。
本提供的文档依据自由软件基金会(Free Software Foundation)发布的GNU自由文档许可(v1.3)条款许可。
Qt及其相关标志是芬兰和/或世界其他地区的The Qt Company Ltd.的商标。所有其他商标均为其各自所有者的财产。