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

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

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

我一直都是測試驅動開發的粉絲,令我失望的是,乍一看,TDD和GWT似乎無法協同工作。

測試GWT程式碼有點問題。問題的核心是GWT程式碼在執行前被編譯成javascript。在許多情況下,GWT.create()語句用於掛接到動態繫結機制。當在正常的Java環境中執行時,該語句會導致異常。

即使你使用像EasyMock這樣的mocking庫來模擬罪魁禍首,如果你從建構函式中觸發它,例如,你仍然很可能會遇到問題。為所有小部件建立介面開銷太大,即使你這樣做了,你也無法測試包含GWT.create()的特定類。

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

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

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

  • 它們立即從IDE執行(無需切換,無需等待)
  • 它們執行速度快(上限在10秒量級)
  • 它們可以獨立測試單元(模擬或存根出依賴項)
所有這些要求都與快速輕鬆地在測試模式和開發模式之間切換有關。因為如果可以做到這一點,你就可以將全部注意力集中在解決問題上。根據個人經驗,我可以說這是一種進入心流的可靠方法,但你的體驗可能會有所不同。無論如何,如果這些基本要求沒有得到滿足,你在開發過程中分心,這消除了預先測試的優勢,而這正是TDD的精髓。

那麼我們如何做才能防止程式碼因缺乏測試而變得一團糟,同時將那些緩慢的GWTTestCase保持在最低限度呢?MVC來拯救

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

我更簡短的總結是:檢視通常很難測試,因此它們應該知道和做盡可能少的事情。在GWT中,檢視是Widget的同義詞。現在這裡有一個小問題:任何在客戶端上執行的東西都歸Widget所有。大多數GWT程式碼的邏輯幾乎都在小部件中,所以大多數使用GWT的開發人員會擴充套件一個小部件並新增更多邏輯。

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

如果你不將邏輯放入檢視(Widget子類)中,你需要另一個地方來放置它。這就是控制器發揮作用的地方。回到我之前提到的問題,我們需要從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 社群所有即將舉行的活動。

檢視所有