Grails 2.0 倒數計時:單元測試

工程 | Peter Ledbrook | 2011 年 6 月 7 日 | ...

Grails 1.4 的第一個里程碑 (現在是 2.0) 已經發布,我們正處於邁向 1.4 2.0 最終版本的最後階段。 隨著我們接近那個時間點,我將撰寫一系列部落格文章,涵蓋 1.4 2.0 版本帶來的各種新功能和變更。 我將從新的測試支援開始。

從一開始,Grails 就為開發人員提供了三個層次的測試支援:單元、整合和功能。 單元測試過去和現在都具有獨立於 Grails 運行的優點,但它們通常需要相當多的額外工作來進行模擬 (mocking)。 Grails 1.1 引入的單元測試框架有助於進行模擬,但它仍然沒有涵蓋所有使用案例,因此開發人員需要比預期更早地求助於在已啟動的 Grails 實例中運行的整合測試。

Grails 2.0 引入了顯著的變更,大大改善了這種情況

  • 單元測試支援可以整合到任何測試框架中(不再需要基底類別);
  • 它具有完整的記憶體內 GORM 實作;並且
  • 它更好地支援測試 REST 動作、檔案上傳等等。

那麼,這些變更對您作為使用者來說是什麼樣子?

繼承的終結

最初的單元測試支援是以類別層級結構的形式提供的,您自己的測試案例必須擴展這些類別,其根目錄是GrailsUnitTestCase。 這是 JUnit 早期的一個歷史悠久的模式,並且廣為人知。 它最初也適用於 Grails。 當人們切換到 JUnit 3 以外的測試框架(例如 Spock)時,問題開始出現,Spock 也需要您繼承基底類別spock.lang.Specification.

眾所周知,Java 不支援多重繼承,因此 Spock 的結果是基於GrailsUnitTestCase基於Specification類別的層級結構的重複。 這並非理想狀態!

Grails 2.0 透過註解提供最初由GrailsUnitTestCase及其家族提供的所有功能,從而解決了這個問題。 因此,對於一個簡單的控制器單元測試,您現在有這樣的程式碼

package org.example

import grails.test.mixin.*

@TestFor(PostController)
class PostControllerTests {
    void testIndex() {
        controller.index()
        assert "/post/list" == response.redirectedUrl
    }
    ...
}

如您所見,添加TestFor註解會立即讓controllerresponse變數(以及其他變數)可用於您的測試。 而且所有這些都不需要extends! 更好的是,使用最新的 Spock 外掛程式,您還可以執行

package org.example

import grails.test.mixin.*

@TestFor(PostController)
class PostControllerSpec extends spock.lang.Specification {
    def "Index action should redirect to list page"() {
        when: "The index action is hit"
        controller.index()

        then: "The user should be redirected to the list action"
        response.redirectedUrl == "/post/list"
    }
    ...
}

換句話說,無論您使用哪種測試框架,您都可以立即利用單元測試支援的任何改進。 如果您願意,您仍然可以使用舊的GrailsUnitTestCase層級結構,但它不支援任何新功能。 因此,我們強烈建議您盡快將測試遷移到基於註解的機制。

我在說什麼新功能? 真正的 GORM 實作怎麼樣。

記憶體內 GORM 實作

自從引入單元測試框架以來,它就支援對領域類別進行模擬。 這使您免於明確地模擬各種動態方法的麻煩,例如save()list()。 但它從來都不是完整的 GORM 實作,使用者必須知道其限制才能有效地使用它。 特別是,標準查詢必須手動模擬,並且新的 GORM 方法通常在模擬實作中落後。

GORM API 的引入改變了這種情況:現在可以實作這個 API 並針對 TCK 檢查該實作。 只要 TCK 測試通過,該實作就符合 GORM 標準。 並且由於 GORM 的 noSQL 工作,我們現在有一個記憶體內 GORM 實作,可用於單元測試。

那麼,您如何在測試中使用這個 GORM 實作? 很簡單! 只需在一個新的註解中宣告您要測試的領域類別@Mock。 然後,您可以像在正常的 Grails 程式碼中一樣與這些領域類別的實例進行互動。 例如,考慮list動作的PostController我們要測試的。 這個動作將對Post領域類別執行查詢,並且我們想要確保它返回適當的領域實例。 以下是如何使用新的單元測試支援來做到這一點

package org.example

import grails.test.mixin.*

@TestFor(PostController)
@Mock(Post)
class PostControllerTests {
    void testList() {
        new Post(message: "Test").save(validate: false)
        def model = controller.list()

        assert model.postInstanceList.size() == 1
        assert model.postInstanceList[0].message == "Test"
        assert model.postInstanceTotal == 1
    }
}

兩個關鍵行已突出顯示:@Mock註解和Post.save()行。 前者確保Post的行為像一個正常的領域類別,而後者保存一個新的Post實例。 然後,該實例將被index動作執行的查詢選取。 如您所見,不需要mockDomain()方法,只需直接、廣為人知的 GORM 程式碼。

您可能會問的一個問題是,為什麼上面的範例在儲存新的領域實例時使用validate: false選項? 您必須記住,您正在使用完整的 GORM 實作,因此驗證預設情況下會生效。 對於一個簡單的領域類別來說,這不是問題,但是如果您有數十個屬性和一些必需的關係呢? 建立有效的領域實例圖可能需要相當多的努力,但正在測試的方法或動作可能只存取領域類別的一個或兩個屬性。 停用驗證會消除原本繁瑣的要求。

例如,假設Post領域類別有一個必需的user屬性,類型為User。 現在,list動作根本不關心使用者 - 它只是返回一個帖子列表。 但是,如果啟用了驗證,您必須建立一個虛擬User實例並將其附加到Post實例。 將其擴展到一個複雜的領域模型,您會發現驗證在這種特殊情況下不是您的朋友。

這個“模擬”GORM 實作甚至擴展到標準查詢,因此您現在可以輕鬆地從單元測試案例中測試這些查詢。 並且由於我們有 GORM TCK,因此對 GORM 的任何變更都會立即反映在模擬實作中。 在 Grails 中使用領域類別進行單元測試從未如此簡單!

在我繼續之前,還有一件事需要注意。 GORM 實作尚未完全支援交易,因此如果您有任何要測試的withTransaction區塊,您仍然必須依賴整合或功能測試。 這並不意味著您不能對使用withTransaction的程式碼進行單元測試 - 您可以 - 但您將無法可靠地測試交易語意。 對於大多數人來說,尤其是那些使用交易服務的人來說,這根本不是問題。

GORM 模擬只是單元測試支援的一項改進。 過去難以實現的其他幾種場景現在也得到了簡化。

其餘的

您是否嘗試過對 JSON 回應進行單元測試? Grails 篩選器? 標籤庫? 雖然這些都是可能的,但並不容易,並且經常需要相當多的模擬。 Grails 2.0 引入了一系列變更,使這些測試(以及更多)變得更加容易。 所有可能性都記錄在使用者指南中,因此我只會在此處重點介紹一些場景來激發您的興趣。

測試 XML/JSON 回應

隨著 REST 似乎如此普遍,越來越多的 Grails 應用程式可能會使用“render as XML/JSON”選項。 但是你如何對這些進行單元測試呢? 假設list動作PostController看起來像這樣

    def list = {
        params.max = Math.min(params.max ? params.int('max') : 10, 100)

        def postList = Post.list(params)
        withFormat {
            html {
                [postInstanceList: postList, postInstanceTotal: Post.count()]
            }
            xml {
                render(contentType: "application/xml") {
                    for (p in postList) {
                        post(author: p.author, p.message)
                    }
                }
            }
            json {
                render(contentType: "application/json") {
                    posts = postList.collect { p ->
                        return { message = p.message; author = p.author }
                    }
                }
            }
        }
    }

首先,您需要設定您想要測試的格式,以便withFormat選擇適當的程式碼區塊。 然後,您必須以某種方式檢查是否生成了正確的 JSON 字串。 兩者都可以透過自動注入到控制器單元測試案例中的response屬性輕鬆實現

    void testListWithJson() {
        new Post(message: "Test", author: "Peter").save()
        response.format = "json"
        controller.list()

        assert response.text == '{"posts":[{"message":"Test","author":"Peter"}]}'
    }

當然,比較字串通常非常脆弱。 對於像上面這樣的小型 JSON 回應來說,這很好,但是如果控制器突然在 JSON 回應中包含dateCreated屬性呢? 上面的測試會立即失敗。 這可能是您想要的,但也許您不關心是否包含dateCreated

幸運的是,您也可以像查詢物件層級結構一樣查詢 JSON 回應,而不是直接查詢字串。response物件同時具有jsonxml屬性,這些屬性是底層 JSON 或 XML 的物件表示

    void testListWithJson() {
        ...
        assert response.json.posts.size() == 1
        assert response.json.posts[0].message == "Test"
    }

這可以使您的單元測試更易於維護和更健壯,並且絕對可以透過僅查看 JSON 或 XML 文件的一部分來測試大型回應。

標籤庫

當涉及到自定義標籤時,生活肯定變得更輕鬆了。 您之前可以測試它們,但是對另一個標籤的任何調用都必須手動模擬,例如透過mockFor()。 對於簡單的標籤來說,這還可以,但對於更複雜的標籤來說,很快就會變成一種負擔。

所以改變了什麼呢? 首先,單元測試現在更像是整合測試,因為您可以使用applyTemplate()方法,並搭配您正在測試的標籤的標記形式。 其次,您不必模擬對其他自定義標籤的調用。 標準的 Grails 標籤可以直接使用,並且您可以透過簡單地調用mockTagLib()並提供相關的TagLib類別來啟用其他標籤。

舉例來說,考慮以下這些非常簡單的標籤:

package org.example

class FirstTagLib {
    static namespace = "f"

    def styledLink = { attrs, body ->
        out << '<span class="mylink">' << s.postLink(attrs, body) << '</span>'
    }
}

class SecondTagLib {
    static namespace = "s"

    def postLink = { attrs, body ->
        out << g.link(controller: "post", action: "list", body)
    }
}

這個<f:styledLink>標籤會調用<s:postLink>標籤,而這個標籤又會調用標準的<g:link>標籤。 因此,如果我們想測試<f:styledLink>標籤,我們會模擬SecondTagLib以確保<s:postLink>可以正常運作,然後像這樣執行:applyTemplate()(程式碼範例,略)

package org.example

import grails.test.mixin.*

@TestFor(FirstTagLib)
class FirstTagLibTests {
    void testStyledLink() {
        mockTagLib(SecondTagLib)
        assert applyTemplate('<f:styledLink>Test</f:styledLink>') == '<span class="mylink"><a href="/post/list">Test</a></span>'
    }
}

如您所見,Grails 為您做了很多繁重的工作,確保標籤調用鏈像在應用程式中一樣運作。 您需要記住的一件事是,像這樣的標籤<g:link>將假定 Servlet 上下文為 "",這就是為什麼上面的例子檢查href值是否為 "/post/list" 而不是 "/my-app/post/list" 的原因。

這些只是改進的單元測試支持的兩個例子。 其他包括:

  • Grails Filters(Grails 篩選器)
  • File uploads(檔案上傳)
  • Command objects(命令物件)
  • View and template rendering(視圖和模板渲染)
如您所見,現在幾乎沒有 Grails 程式碼區域不能進行單元測試。

結論

測試一直是應用程式開發的重要組成部分,測試的難易程度直接影響測試覆蓋率:編寫測試越容易,開發人員就越有可能編寫測試。 這就是為什麼 Grails 2.0 版本帶來的單元測試變更如此重要。 它們使為過去相對棘手的場景編寫單元測試變得容易得多。 這樣,平均 Grails 應用程式的測試覆蓋率可能會提高,開發人員最終將獲得更強大的應用程式。

所有這些都使得單元測試改進成為 Grails 2.0 最重要的功能之一,也是升級的有力論據。 所以下載最新的 2.0 版本並試用一下吧!

取得 Spring 電子報

隨時關注 Spring 電子報

訂閱

領先一步

VMware 提供培訓和認證,以加速您的進展。

了解更多

獲得支援

Tanzu Spring 在一個簡單的訂閱中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位檔案。

了解更多

即將舉行的活動

查看 Spring 社群中所有即將舉行的活動。

查看所有