在 GWT 客戶端程式碼中啟用測試驅動開發

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

在過去幾個月裡,我一直與各種客戶合作開展使用 Google Web Toolkit [GWT] 的專案。我喜歡 GWT,主要是因為它提供了 Java 到 javascript 的編譯器。這是讓普通 Java 開發人員無需學習新語言即可建立 RIA 的關鍵。

我一直以來都是測試驅動開發(TDD)的擁躉,令我失望的是,乍一看,GWT 和 TDD 似乎無法協同工作。

測試 GWT 程式碼有些問題。核心問題在於 GWT 程式碼在執行前被編譯成 javascript。在許多情況下,GWT.create() 語句用於連線動態繫結機制。在普通的 Java 環境中執行時,這個語句會導致異常。

即使你使用像 EasyMock 這樣的模擬庫來模擬出導致問題的部分,如果它是在建構函式中觸發的,你仍然可能遇到問題。為所有 widget 建立介面開銷太大了,而且即使你這樣做了,你也無法測試包含 GWT.create() 的特定類。

GWT 在 GWTTestCase 中提供了這個問題的解決方案,但這個類本身也存在不少問題。僅舉幾例:

  • 它在測試執行器中啟動託管模式(使其變慢)
  • 它需要在 GWT 編譯的程式碼上執行(使其變慢)
  • 它會等待非同步服務並設定超時(使其變慢)
它提供了一個不錯的機制來對客戶端程式碼進行整合測試,但對於單元測試來說過於臃腫。

當你進行測試驅動開發時,你需要能夠編寫至少具有以下特性的測試:

  • 它們可以直接從 IDE 執行(無需切換、無需等待)
  • 它們執行速度快(上限在 10 秒量級)
  • 它們可以隔離地測試單元(模擬或樁出依賴項)
所有這些要求都關乎在測試模式和開發模式之間快速輕鬆地切換。因為如果能夠做到這一點,你就能完全專注於解決問題。根據個人經驗,我可以斷言這是進入心流狀態的可靠方法,但效果可能因人而異。無論如何,如果這些基本要求**未能**滿足,你在開發過程中**將會**分心,從而失去預先測試的優勢,而這正是 TDD 的精髓。

那麼,我們該怎麼做才能避免因為缺乏測試導致程式碼變成“麵條”,同時儘量減少那些緩慢的 GWTTestCases 呢? MVC 來拯救!

MVC 是一個久經考驗的模式。它有很多應用,其中一些除了名字之外與原始 MVC 模式幾乎沒有什麼共同點,但我想指出的總體方向最好由 Martin Fowler 在 Humble View(謙遜檢視)這個名字下總結。

我更簡短的總結是這樣的:檢視通常很難測試,因此它們應該知道得越少越好,做得也越少越好。在 GWT 中,檢視等同於 Widget。這裡有一個小問題:任何在客戶端執行的東西都由 Widget 擁有。大多數 GWT 程式碼幾乎把所有的邏輯都放在了 widget 中,所以大多數使用 GWT 的開發者都透過繼承一個 widget 然後新增一些邏輯來完成工作。

我的建議很簡單:不要那樣做。型別安全對於獲得 IDE 支援來說很好,但它並不能阻止你寫出糟糕的程式碼。所以如果你不做單元測試,你的程式碼最終會變得一團糟。

如果你不把邏輯放在檢視(Widget 的子類)中,你需要另一個地方來放它。這就是 Controller 發揮作用的地方。回到我之前提到的小問題,我們將需要從 Widget 或 EntryPoint 中引導 Controller。這確實是無法避免的,所以我們就這樣做,看看它看起來有多糟。

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 用於將 Controller 與 Window 解耦,以便處理偶爾的彈窗。我覺得這還不算太糟,不為此程式碼寫測試我也能睡個好覺。問題還沒有完全解決,因為我們想在檢視中使用的任何元素(按鈕、標籤等)都需要在檢視中例項化,然後註冊到 Controller 中。

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")));

Controller 透過它們的介面 (SourcesClickEvents) 接受按鈕,這帶來的好處是我們可以用外觀不同的 Widget 替換按鈕,而無需修改 Controller。這沒什麼新鮮的,這正是 MVC 所倡導的關注點分離。老實說,我通常更喜歡寫一個測試來檢查註冊是否正確發生,但這是沒有 GWTTesCase 就無法做到的。現在是時候讓我們的 IDE 為我們建立那個 Controller + 方法,並編寫測試,這樣我們就可以實現邏輯了。例如,載入按鈕的測試看起來是這樣的:

    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 社群的所有近期活動。

檢視全部