Grails 2.0 倒计时:单元测试

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

Grails 1.4 的第一个里程碑(现在是 2.0)现已发布,我们正处于通往1.4 2.0 正式的最后阶段。随着我们接近那个点,我将撰写一系列博客文章,涵盖1.4 2.0 版本带来的各种新功能和更改。我将从新的测试支持开始。

从一开始,Grails 就为开发人员提供了三个级别的测试支持:单元、集成和功能。单元测试过去和现在都具有独立于 Grails 运行的优势,但它们通常需要相当多的额外工作,例如模拟。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 应用程序可能会使用“呈现为 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 过滤器
  • 文件上传
  • 命令对象
  • 视图和模板渲染
如您所见,现在很少有 Grails 代码区域无法进行单元测试。

结论

测试一直是应用程序开发的重要组成部分,测试的便捷性直接影响测试覆盖率:测试越容易编写,开发人员就越有可能编写它们。这就是 Grails 2.0 版本带来的单元测试更改如此重要的原因。它们使为以前相对棘手的场景编写单元测试变得更加容易。这样,平均 Grails 应用程序的测试覆盖率可能会提高,开发人员最终将获得更强大的应用程序。

所有这些都使单元测试改进成为 Grails 2.0 最重要的功能之一,也是升级的有力论据。因此,下载最新的 2.0 版本并试一试!

获取 Spring 新闻通讯

与 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,以加速您的进步。

了解更多

获取支持

Tanzu Spring 在一个简单的订阅中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区中所有即将举行的活动。

查看全部