如下所示
正如你所見,Grails 為你做了很多繁重的工作,確保標籤呼叫的鏈式結構能夠像在應用程式中一樣工作。你需要記住的一點是,像
<g:link>Grails 1.4 (現為 2.0) 的第一個里程碑版本現已釋出,我們正處於通往 2.0 最終版本的最後階段。隨著這個節點的臨近,我將撰寫一系列博文,介紹 2.0 版本帶來的各種新特性和變化。我將從新的測試支援開始。
從一開始,Grails 就為開發者提供了三種級別的測試支援:單元測試、整合測試和功能測試。單元測試過去和現在都具有獨立於 Grails 執行的優點,但它們通常需要透過模擬進行大量的額外工作。Grails 1.1 引入的單元測試框架有助於進行模擬,但它仍然沒有涵蓋所有用例,因此開發者不得不比預期更早地求助於整合測試,整合測試在啟動的 Grails 例項內部執行。
Grails 2.0 引入了重大變化,極大地改善了這種情況
那麼,這些變化對使用者來說是什麼樣的呢?
最初的單元測試支援是以類層次結構的形式提供的,你自己的測試用例必須繼承這些類,其根是GrailsUnitTestCase。這是 JUnit 早期的一個歷史悠久的模式,它很容易理解。最初它對 Grails 也執行良好。問題始於人們轉向 JUnit 3 以外的測試框架,例如 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 實現,使用者必須瞭解其侷限性才能有效使用它。特別是,criteria 查詢必須手動模擬,新的 GORM 方法通常在模擬實現中滯後。
GORM API 的引入改變了現狀:現在可以實現這個 API,並根據 TCK 檢查該實現。只要 TCK 測試透過,該實現就符合 GORM 標準。由於 GORM 的 noSQL 工作,我們現在有了一個可用於單元測試的記憶體中 GORM 實現。那麼你如何在測試中使用這個 GORM 實現呢?很簡單!只需在一個新的註解@Mock中宣告你想測試的領域類。然後你就可以像在正常的 Grails 程式碼中一樣與這些領域類的例項進行互動。例如,考慮我們正在測試的PostController的list操作。這個操作將對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
}
}
領域類執行查詢,我們想確保它返回了適當的領域例項。以下是使用新的單元測試支援實現的方法那麼你如何在測試中使用這個 GORM 實現呢?很簡單!只需在一個新的註解突出顯示了兩行關鍵程式碼:@Mock註解和操作。這個操作將對Post.save()操作。這個操作將對這行程式碼。前者確保Post表現得像一個普通的領域類,而後者則儲存了一個新的Post例項。然後,該例項將被
index操作執行的查詢檢索到。正如你所見,無需mockDomain()
方法,只需直接、易懂的 GORM 程式碼即可。操作。這個操作將對你可能會問一個問題:為什麼上面的例子在儲存新的領域例項時使用了validate: false選項?你必須記住,你正在操作一個完整的 GORM 實現,因此驗證預設會生效。對於一個簡單的領域類來說這不是問題,但如果你有幾十個屬性和一些必需的關係呢?構建一個有效的領域例項圖可能需要付出相當大的努力,然而正在測試的方法或操作可能只訪問領域類的一兩個屬性。停用驗證移除了本來會是一個繁重的要求。例如,假設Post中宣告你想測試的領域類。然後你就可以像在正常的 Grails 程式碼中一樣與這些領域類的例項進行互動。例如,考慮我們正在測試的領域類有一個必需的例如,假設user操作。這個操作將對屬性,型別為
User
。現在,index操作完全不關心使用者——它只是返回一個帖子列表。但是如果啟用了驗證,你就必須建立一個虛擬的indexUser
例項並將其附加到
例項。將其擴充套件到複雜的領域模型,你就會看到驗證在這種特定情況下不是你的朋友。
在繼續之前,還有一件事需要注意。GORM 實現尚未完全支援事務,因此如果你有任何中宣告你想測試的領域類。然後你就可以像在正常的 Grails 程式碼中一樣與這些領域類的例項進行互動。例如,考慮我們正在測試的withTransaction的程式碼塊需要測試,你仍然需要依賴整合測試或功能測試。這並不意味著你不能單元測試使用
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 }
}
}
}
}
}
withTransaction的程式碼——你可以——但你無法可靠地測試事務語義。對於大多數人來說,特別是那些轉而使用事務性服務的人來說,這根本不是問題。GORM 模擬只是單元測試支援的一項改進。其他一些過去很困難的場景現在也得到了簡化。response其他
void testListWithJson() {
new Post(message: "Test", author: "Peter").save()
response.format = "json"
controller.list()
assert response.text == '{"posts":[{"message":"Test","author":"Peter"}]}'
}
你是否曾經嘗試過單元測試 JSON 響應?Grails 過濾器?標籤庫?雖然這些都可以實現,但並不特別容易,而且通常需要相當多的模擬。Grails 2.0 帶來了許多變化,使得此類測試(以及更多)顯著變得容易。所有可能性都在使用者指南中記錄,所以我在此僅重點介紹幾個場景,以激發你的興趣。測試 XML/JSON 響應隨著 REST 似乎如此普及,越來越多的 Grails 應用程式可能會使用“渲染為 XML/JSON”選項。但是你如何對這些進行單元測試呢?假設測試 XML/JSON 響應PostController
的responseindex操作如下所示和首先,你需要設定你想測試的格式,以便withFormat
void testListWithJson() {
...
assert response.json.posts.size() == 1
assert response.json.posts[0].message == "Test"
}
選擇適當的程式碼塊。然後你必須設法檢查是否生成了正確的 JSON 字串。這兩者都可以透過
自動注入到控制器單元測試用例中的屬性輕鬆實現當然,比較字串通常非常脆弱。對於像上面那樣的小 JSON 響應來說沒問題,但如果控制器突然在 JSON 響應中包含了dateCreated
屬性呢?上面的測試將立即失敗。這可能正是你想要的,但也可能你並不關心dateCreated是否包含?幸運的是,你也可以像操作物件層次結構一樣查詢 JSON 響應,而不是直接將其視為字串。response物件同時具有json
和
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)
}
}
xml屬性,它們是底層 JSON 或 XML 的物件表示這可以使你的單元測試更易於維護和更健壯,並且無疑使得僅檢視 JSON 或 XML 文件的部分內容來測試大型響應成為可能。標籤庫關於自定義標籤,事情無疑變得更容易了。你之前可以測試它們,但對其他標籤的任何呼叫都必須手動模擬,例如透過mockFor()。對於簡單標籤來說這沒問題,但對於更復雜的標籤,這可能會迅速成為負擔。屬性,它們是底層 JSON 或 XML 的物件表示那麼有什麼變化呢?首先,單元測試現在更像是整合測試,因為你使用了applyTemplate()方法,並使用你正在測試的標籤的標記形式。其次,你不必模擬對其他自定義標籤的呼叫。標準的 Grails 標籤將直接工作,你只需呼叫標籤庫mockTagLib()dateCreated並附帶相關的
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>'
}
}
TagLibmockFor()類,即可啟用其他標籤。舉個例子,考慮這些非常簡單的標籤標籤呼叫了
<f:styledLink>
SecondTagLib
以確保