在GWT客户端代码中启用测试驱动开发

工程 | Iwein Fuld | 2008年2月19日 | ...

在过去的几个月里,我一直在与不同的客户合作,参与使用Google Web Toolkit [GWT]的项目。我主要喜欢GWT是因为它的Java到Javascript编译器。这是关键,让普通的Java开发者能够创建RIA,而无需学习新的语言。

我一直以来都是测试驱动开发的粉丝,令我失望的是,起初看起来TDD和GWT似乎无法兼容。

测试GWT代码有点棘手。问题的核心在于GWT代码在运行前被编译成Javascript。在很多情况下,使用GWT.create()语句来连接动态绑定机制。在普通的Java环境中执行此语句会导致异常。

即使你使用EasyMock之类的模拟库来模拟罪魁祸首,如果它例如是从构造函数触发的,你仍然可能遇到这个问题。为所有小部件创建接口工作量太大,即使你这样做了,你也无法测试包含GWT.create()的特定类。

GWT在GWTTestCase中为此问题提供了解决方案,但这个类本身也有一些问题。仅举几例:

  • 它从测试运行器内部启动托管模式(使其变慢)
  • 它需要在GWT编译的代码上运行(使其变慢)
  • 它使用超时等待异步服务(使其变慢)
它提供了一个相当不错的机制来对你的客户端代码进行集成测试,但是对于单元测试来说,它过于臃肿。

当你进行测试驱动开发时,你需要能够编写至少具有以下特性的测试:

  • 它们可以立即从IDE运行(无需切换,无需等待)
  • 它们运行速度快(上限在10秒的数量级)
  • 它们可以隔离地测试单元(模拟或存根依赖项)
所有这些需求都与快速轻松地在测试模式和开发模式之间切换有关。因为如果这是可能的,你可以将全部精力集中在解决问题上。根据我的个人经验,我可以说这是进入心流状态的可靠方法,但你的里程可能会有所不同。无论如何,如果这些基本要求没有得到满足,你将会在开发过程中分心,这将消除前期测试的优势,而前期测试是TDD的本质。

那么,我们如何才能防止代码因缺乏测试而变成意大利面条代码,同时又将那些缓慢的GWTTestCases降到最低呢?**MVC来救援**

MVC是一个久经考验的模式。它有很多应用,其中一些与原始MVC模式除了名称外几乎没有任何共同之处,但我想要指出的总体方向最好由Martin Fowler用“Humble View”(谦逊的视图)来概括。

我更简短的总结是这样的:视图通常很难测试,因此它们应该尽可能少地了解和做。在GWT中,视图与小部件同义。现在这里有一个小小的陷阱:在客户端运行的任何东西都由小部件拥有。大多数GWT代码几乎所有的逻辑都在小部件中,因此大多数使用GWT的开发人员扩展一个小部件并只添加一些额外的逻辑。

我的建议很简单:不要那样做。类型安全有助于获得IDE支持,但它并不能阻止你编写糟糕的代码。所以,如果你不进行单元测试,你的代码最终会一团糟。

如果你不走将逻辑放入你的视图(小部件子类)的道路,你需要另一个地方来放置它。这就是控制器发挥作用的地方。回到我之前提到的陷阱,我们需要从小部件或EntryPoint引导控制器。这实际上是无法避免的,所以让我们这样做,看看它看起来有多糟糕。

public class NoteEditor extends Composite {
    public NoteEditor() {
        //do the dependency injection stuff
        NoteModel noteModel = new NoteModel();
        NoteEditorController controller =
                new NoteEditorController(noteModel, NoteService.App.getInstance(), new AlertCallback());
        //...
    }
}

正如你所看到的,我在这里做了一些依赖注入,因为我们还没有在客户端使用Spring。我说“还没有”,因为我们以后可以使用GWToolbox或Rocket来实现这一点。服务使用GWT.create()来引导,AlertCallback用于将控制器与Window解耦,以便偶尔发出警报。我觉得这还不错,我可以轻松地入睡而无需为此代码编写测试。麻烦还没有完全结束,因为我们想要在视图中使用的任何元素(按钮、标签等)都需要在视图中实例化,然后向控制器注册。

controller.registerDetailViewSelector(new DeckPanelSelector(detailView));
detailView.addStyleName("detailPanel");
main.add(detailView, DockPanel.CENTER);
//some buttons at the bottom of the screen
buttonPanel.add((Widget)controller.registerClearButton(new Button("Clear")));
buttonPanel.add((Widget)controller.registerLoadButton(new Button("Load existing Note")));

控制器通过它们的接口(SourcesClickEvents)接受按钮,这作为一个额外的好处,允许我们将按钮替换为具有不同外观的小部件,而无需更改我们的控制器。这里没有什么新的,这正是MVC关注点分离的重点。说实话,我通常更喜欢编写一个测试来检查注册是否正确完成,但这在没有GWTTesCase的情况下是无法完成的。现在是时候让我们的IDE为我们创建控制器+方法并编写测试,以便我们可以实现逻辑了。例如,加载按钮的测试看起来像这样:

    public void testLoadButtonPressed_success() throws Exception {
        final Foo expectedFoo = new Foo("expected");
        fooServiceMock.loadFoo(isA(String.class), isA(AsyncCallback.class));
        expectLastCall().andAnswer(new IAnswer() {
            public Object answer() throws Throwable {
                ((AsyncCallback) getCurrentArguments()[1]).onSuccess(expectedFoo);
                return null;
            }
        });
        fooModelMock.setFoo(expectedFoo);
        replay(allMocks);
        loadButton.fireClick();
        verify(allMocks);
    }

我们开始了!我附加了一个简短的示例来展示这个想法。你可以将其用作实验的起点。

2008年10月更新:示例的源代码已过时。一般的建议仍然有效。

总而言之,这是一个GWT风格的MVC模式的简要概述。但是,你当然可以选择你自己的风格,只要你将你的逻辑放在可测试的类中

GWT flavored MVC pattern

获取Spring新闻通讯

通过Spring新闻通讯保持联系

订阅

领先一步

VMware提供培训和认证,以加速你的进步。

了解更多

获得支持

Tanzu Spring在一个简单的订阅中提供对OpenJDK™、Spring和Apache Tomcat®的支持和二进制文件。

了解更多

即将举行的活动

查看Spring社区中所有即将举行的活动。

查看全部