领先一步
VMware 提供培训和认证,以加速您的进步。
了解更多Grails 1.4 的第一个里程碑(现在是 2.0)现已发布,我们正处于通往1.4 2.0 正式的最后阶段。随着我们接近那个点,我将撰写一系列博客文章,涵盖1.4 2.0 版本带来的各种新功能和更改。我将从新的测试支持开始。
从一开始,Grails 就为开发人员提供了三个级别的测试支持:单元、集成和功能。单元测试过去和现在都具有独立于 Grails 运行的优势,但它们通常需要相当多的额外工作,例如模拟。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 应用程序可能会使用“呈现为 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 版本并试一试!