窗口驱动程序

提供一个编程 API 来驱动和查询 UI 窗口。

2004 年 8 月 26 日

这是我在 2000 年代中期进行的 进一步的企业应用程序架构开发 写作的一部分。不幸的是,此后太多其他事情吸引了我的注意力,所以我没有时间进一步研究它们,而且在可预见的未来我也没有看到太多时间。因此,这些材料非常草稿形式,我不会进行任何更正或更新,直到我有时间再次处理它。

用户界面窗口充当系统的重要网关。尽管它在计算机上运行,但它通常并不真正对计算机友好 - 特别是,它通常很难对其进行编程以自动执行任务。这对测试来说尤其是一个问题,因为自动化测试在简化整个开发过程方面起着重要作用。

窗口驱动程序 是 UI 窗口的编程 API。一个 窗口驱动程序 应该允许程序控制窗口的所有动态方面,调用任何操作并检索对人类用户可用的任何信息。

工作原理

对于 窗口驱动程序 的基本经验法则是,它应该允许软件客户端执行任何操作并查看人类可以执行的任何操作。它还应该提供一个易于编程的接口,并隐藏窗口中的底层小部件。因此,要访问文本字段,您应该具有接受和返回字符串的访问器方法,复选框应该使用布尔值,按钮应该用面向操作的方法名称表示。 窗口驱动程序 应该封装实际操作 GUI 控件本身中的数据所需的机制。一个好的经验法则是想象更改具体控件 - 在这种情况下, 窗口驱动程序 接口不应该改变。

窗口驱动程序 接口中的名称应该反映可见的 UI,因此根据屏幕上的标签命名元素。

富客户端窗口通常将其控件组织成一个复杂的层次结构。当您使用基于流的布局而不是绝对坐标布局时,尤其如此。 窗口驱动程序 应该尽可能地从接口中隐藏布局设计。这样,对内部布局的更改就不会导致客户端发生更改。对此的一个例外可能是使用选项卡或侧边栏选择器等技术的多分区窗口。在这种情况下,控件的数量众多使得将编程 API 分成单独的 窗口驱动程序 类变得很有意义。

使用现代 UI 的 窗口驱动程序 的更棘手方面之一是处理多个线程。通常,一旦启动窗口,它就会在与驱动程序不同的线程上运行。这会导致令人讨厌的线程错误滋生,从而使 窗口驱动程序 变得不可靠。有几种方法可以解决这个问题。一种是使用一个库,该库会将请求放到 UI 线程上。另一种是不实际启动窗口,这样它就不会实际进入 UI 线程。

窗口驱动程序 的接口可以公开小部件本身,也可以公开在小部件上进行您感兴趣的更改的方法。因此,如果您想在 swing 中公开一个文本字段,您可以通过单个方法 JTextField getArtistField(); 或通过字段的不同方面的各种方法来实现 String getArtistField(), void setArtistField(String text), bool getArtistFieldEnabled()。返回字段本身更简单,但这确实意味着 窗口驱动程序 的客户端依赖于窗口系统,并且使用 窗口驱动程序 的程序员需要熟悉窗口系统的运作方式。这也意味着窗口系统必须在对小部件的更改(通过直接调用它们的方法来完成)时触发事件。总的来说,我更喜欢返回小部件,除非有充分的理由不这样做。

何时使用它

窗口驱动程序 最常见的用途是用于测试,特别是在使用 自主视图 时。 窗口驱动程序 将测试与视图实现的细节隔离开来,简化了测试的编写并将它们与视图组织中的更改隔离开来。

窗口驱动程序 还可以用于在应用程序之上提供脚本接口。但是,在大多数情况下,最好在系统的较低层编写这样的接口。一个可能难以做到这一点的情况是,当您的应用程序有大量行为嵌入到视图中,并且将此行为移动到较低层太困难时。如果可以的话,我仍然更喜欢移动行为。

如果您努力提供一个非常薄的视图,则可能不需要 窗口驱动程序。诸如 监督控制器被动视图演示模型 等模式旨在使 窗口驱动程序 变得不必要。

示例:Swing 相册示例(Java)

这是我用于运行的相册示例的 窗口驱动程序。我的一个要求是我可以编写一组测试,这些测试将使用不同的模式测试此窗口的多个实现。这不会是一个常见的要求,但它有助于说明 窗口驱动程序 如何提供一些实现独立性。

我首先定义 窗口驱动程序 的通用接口

public interface AlbumWindowDriver {
    JList getAlbumList();
    JTextField getTitleField();
    JPanel getMainPane();
    JPanel getAlbumDataPane();
    JPanel getApplyPanel();
    JScrollPane getAlbumListPane();
    JTextField getComposerField();
    JCheckBox getClassicalCheckBox();
    JSplitPane getSplitPane();
    JButton getApplyButton();
    JButton getCancelButton();
    JTextField getArtistField();
    JFrame getWindow();
}

如您所见,这只是公开了用于操作的各种小部件。然后,我在我拥有的各种视图实现中直接实现此接口。

在我的情况下,我使用 Jemmy 驱动测试。测试用例类使用 Jemmy 的运算符来包装 窗口驱动程序 公开的控件。

private JTextFieldOperator title;
private JTextFieldOperator artist;
private JListOperator list;
private JFrameOperator window;
private JCheckBoxOperator isClassical;
private JTextFieldOperator composer;
private JButtonOperator applyButton, cancelButton;
private Album[] albums = (Album[]) Mother.albums().toArray(new Album[Mother.albums().size()]);

protected void setUp() throws Exception {
    AlbumWindowDriver frame = doCreateFrame();
    window = new JFrameOperator(frame.getWindow());
    title = new JTextFieldOperator(frame.getTitleField());
    list = new JListOperator(frame.getAlbumList());
    artist = new JTextFieldOperator(frame.getArtistField());
    isClassical = new JCheckBoxOperator(frame.getClassicalCheckBox());
    composer = new JTextFieldOperator(frame.getComposerField());
    applyButton = new JButtonOperator(frame.getApplyButton());
    cancelButton = new JButtonOperator(frame.getCancelButton());
}
protected abstract AlbumWindowDriver doCreateFrame();

我使用 Jemmy 的运算符来处理线程问题。Jemmy 还提供了允许您在表单层次结构中查找控件的功能,但我不需要这样做,因为 窗口驱动程序 直接将我带到我需要的控件。

抽象方法 doCreateFrame() 在这里,以便我可以使用子类来设置我正在使用的实际视图实现。除非您有这种奇怪的要求,否则您可以直接内联实例化视图。

通过设置各种变量,我现在可以编写直接操作控件的测试。

public void testCheckClassicalBoxEnablesComposerField() {
    list.setSelectedIndex(4);
    assertEquals("Zero Hour", title.getText());
    isClassical.doClick();
    assertTrue(isClassical.isSelected());
    assertTrue("composer field not enabled", composer.isEnabled());
    applyButton.doClick();
    list.setSelectedIndex(0);
    list.setSelectedIndex(4);
    assertTrue("composer field not enabled after switch", composer.isEnabled());
}