倒计时:Grails 2.0:单元测试

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

Grails 1.4 (现为 2.0) 的第一个里程碑版本现已发布,我们正处于通往 2.0 最终版本的最后阶段。随着这个节点的临近,我将撰写一系列博文,介绍 2.0 版本带来的各种新特性和变化。我将从新的测试支持开始。

从一开始,Grails 就为开发者提供了三种级别的测试支持:单元测试、集成测试和功能测试。单元测试过去和现在都具有独立于 Grails 运行的优点,但它们通常需要通过模拟进行大量的额外工作。Grails 1.1 引入的单元测试框架有助于进行模拟,但它仍然没有涵盖所有用例,因此开发者不得不比预期更早地求助于集成测试,集成测试在启动的 Grails 实例内部运行。

Grails 2.0 引入了重大变化,极大地改善了这种情况

  • 单元测试支持可以集成到任何测试框架中(不再需要基类);
  • 它拥有完整的内存中 GORM 实现;并且
  • 它更好地支持测试 REST 操作、文件上传等。

那么,这些变化对用户来说是什么样的呢?

继承消亡之日

最初的单元测试支持是以类层次结构的形式提供的,你自己的测试用例必须继承这些类,其根是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注解会立即使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 实现,用户必须了解其局限性才能有效使用它。特别是,criteria 查询必须手动模拟,新的 GORM 方法通常在模拟实现中滞后。

GORM API 的引入改变了现状:现在可以实现这个 API,并根据 TCK 检查该实现。只要 TCK 测试通过,该实现就符合 GORM 标准。由于 GORM 的 noSQL 工作,我们现在有了一个可用于单元测试的内存中 GORM 实现。那么你如何在测试中使用这个 GORM 实现呢?很简单!只需在一个新的注解@Mock中声明你想测试的领域类。然后你就可以像在正常的 Grails 代码中一样与这些领域类的实例进行交互。例如,考虑我们正在测试的PostControllerlist操作。这个操作将对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

实例并将其附加到

Post

实例。将其扩展到复杂的领域模型,你就会看到验证在这种特定情况下不是你的朋友。

这个“模拟”GORM 实现甚至扩展到了 criteria 查询,因此你现在可以轻松地在单元测试用例中测试它们。而且由于我们有 GORM TCK,对 GORM 的任何更改都将立即反映在模拟实现中。在 Grails 中使用领域类进行单元测试从未如此简单!

在继续之前,还有一件事需要注意。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 字符串。这两者都可以通过

response

自动注入到控制器单元测试用例中的属性轻松实现当然,比较字符串通常非常脆弱。对于像上面那样的小 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>

  • 标签,而后者又调用了标准的
  • <s:postLink>
  • <g:link>
  • 标签。因此,如果我们想测试
StyledLinkTagLib

标签,我们模拟

SecondTagLib

以确保

<s:postLink>

正常工作,然后执行

<f:styledLink>

如下所示

正如你所见,Grails 为你做了很多繁重的工作,确保标签调用的链式结构能够像在应用程序中一样工作。你需要记住的一点是,像

<g:link>

会假定 servlet context 为 "",因此上面的例子检查的是

href

<g:link>

值是否为 "/post/list",而不是 "/my-app/post/list"。

这些只是改进后的单元测试支持的两个例子。其他方面包括

Grails 过滤器