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

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

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

我一直以來都是測試驅動開發的愛好者,但起初讓我失望的是,TDD 和 GWT 似乎無法協同運作。

測試 GWT 程式碼有點問題。問題的核心在於 GWT 程式碼在執行前會編譯為 javascript。在許多情況下,會使用 GWT.create() 語句來連結到動態綁定機制。在正常的 Java 環境中執行此語句會導致例外。

即使您使用像 EasyMock 這樣的模擬程式庫來模擬罪魁禍首,如果它是從建構子觸發的,您仍然很可能遇到問題。為所有 widget 建立介面太過繁瑣,即使您這樣做了,您也將無法測試包含 GWT.create() 的特定類別。

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

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

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

  • 它們可以立即從 IDE 執行(無需切換,無需等待)
  • 它們執行速度快(上限約為 10 秒的數量級)
  • 它們可以隔離測試單元(模擬或樁接相依性)
所有這些要求都是關於快速且輕鬆地在測試模式和開發模式之間切換。因為如果可以做到這一點,您就可以將全部注意力集中在解決問題上。根據個人經驗,我可以說這是一種肯定能進入心流狀態的方法,但您的經驗可能會有所不同。無論如何,如果這些基本要求達成,您在開發過程中會分心,這會消除前期測試的優勢,而這正是 TDD 的精髓。

那麼,我們該怎麼做才能防止我們的程式碼因缺乏測試而變成義大利麵條,同時將那些緩慢的 GWTTestCase 保持在最低限度? MVC 來救援

MVC 是一種歷史悠久的模式。它有很多應用,其中一些應用與原始 MVC 模式幾乎沒有共同之處,除了名稱之外,但我想要指出的總體方向最好由 Martin Fowler 以 Humble View 的名稱概括。

我更簡短的總結是:視圖通常很難測試,因此它們應該盡可能少地了解和執行操作。在 GWT 中,視圖與 Widget 同義。現在這裡有一個小小的陷阱:任何在客戶端運行的東西都歸 Widget 所有。大多數 GWT 程式碼幾乎將所有邏輯都放在 widget 中,因此大多數使用 GWT 的開發人員都會擴展 widget 並僅添加一些更多邏輯。

我的建議很簡單:不要去那裡。類型安全很好,可以獲得 IDE 支援,但它無助於防止您編寫糟糕的程式碼。因此,如果您不進行單元測試,您的程式碼最終會變得一團糟。

如果您不走將邏輯放入視圖(Widget 子類別)的路徑,您將需要另一個地方來放置它。這就是 Controller 的用武之地。回到我之前提到的陷阱,我們需要從 Widget 或 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) 下的按鈕,這作為一個額外的好處,允許我們用一些外觀不同的 Widget 替換按鈕,而無需更改我們的控制器。這裡沒有什麼新鮮事,這正是 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 社群中所有即將到來的活動。

檢視全部