領先一步
VMware 提供培訓和認證,以加速您的進展。
了解更多Grails 1.4 的第一個里程碑 (現在是 2.0) 已經發布,我們正處於邁向 1.4 2.0 最終版本的最後階段。 隨著我們接近那個時間點,我將撰寫一系列部落格文章,涵蓋 1.4 2.0 版本帶來的各種新功能和變更。 我將從新的測試支援開始。
從一開始,Grails 就為開發人員提供了三個層次的測試支援:單元、整合和功能。 單元測試過去和現在都具有獨立於 Grails 運行的優點,但它們通常需要相當多的額外工作來進行模擬 (mocking)。 Grails 1.1 引入的單元測試框架有助於進行模擬,但它仍然沒有涵蓋所有使用案例,因此開發人員需要比預期更早地求助於在已啟動的 Grails 實例中運行的整合測試。
Grails 2.0 引入了顯著的變更,大大改善了這種情況
那麼,這些變更對您作為使用者來說是什麼樣子?
最初的單元測試支援是以類別層級結構的形式提供的,您自己的測試案例必須擴展這些類別,其根目錄是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註解會立即讓controller和response變數(以及其他變數)可用於您的測試。 而且所有這些都不需要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 實作怎麼樣。
自從引入單元測試框架以來,它就支援對領域類別進行模擬。 這使您免於明確地模擬各種動態方法的麻煩,例如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 引入了一系列變更,使這些測試(以及更多)變得更加容易。 所有可能性都記錄在使用者指南中,因此我只會在此處重點介紹一些場景來激發您的興趣。
隨著 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物件同時具有json和xml屬性,這些屬性是底層 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 2.0 版本帶來的單元測試變更如此重要。 它們使為過去相對棘手的場景編寫單元測試變得容易得多。 這樣,平均 Grails 應用程式的測試覆蓋率可能會提高,開發人員最終將獲得更強大的應用程式。
所有這些都使得單元測試改進成為 Grails 2.0 最重要的功能之一,也是升級的有力論據。 所以下載最新的 2.0 版本並試用一下吧!