在TDD已经广为人知的背景下,Grails自然也不会对其视而不见,相反,它为测试提供了大量的支持,简化了测试编写的难度和工作量。
测试有很多种类型,Grails直接支持的有两种:单元测试和集成测试,其他类型的测试都是通过插件来完成的。
单元测试使用grails create-unit-test命令创建,文件保存在/test/unit下;集成测试由grails create-integeration-test创建,文件位于/test/integration。所有create-*命令都会自动创建集成测试,这两类测试的文件名后缀都是Tests。
运行测试的命令是grails test-app,它的用法示例如下:
- 运行全部测试:单元和集成测试。grails test-app
- 运行单个测试:grails test-app 测试名 //不要加Tests后缀
- 运行一组测试:grails test-app test1 test2 …//空格隔开
- 使用通配符:grails test-app some.org.*;grails test-app some.org.**
- 测试某个方法:grails test-app SimpleController.testLogin
测试完成之后会产生测试报告,位于test/reports。
Grails将测试组织成“阶段”和“类型”:grails test-app phase:type
- phase:unit、integration、functional和other
- type:junit针对unit和integration阶段
- 其他的测试插件可能带来其他的阶段和类型
执行junit集成测试的例子:grails test-app integration:integration。phase和type皆可省略,表示所有。grails test-app unit:。更细粒度的指定例子:grails test-app integration: unit: some.org.**。安装其他测试插件可能会引入其他的阶段和类型。
现在,让我们来看看单元测试相关的内容。
Grails不会为单元测试注入任何动态方法,它们在集成测试和运行时被注入。为完成单元测试,开发者需利用Mock。一般的思路是利用:Groovy Mock和ExpandoMetaClass。所幸,但是Grails为单元测试提供了大量的mock方法,简化了单元测试。这些mock是在grails.test.GrailsUnitTestCase中提供的。
在GrailsUnitTestCase众多的mock方法中,首先要提及的就是mockFor,它是通用的Mock方法,使用如下:
def strictControl = mockFor(MyService)
strictControl.demand.someMethod(0..2) { String a, int b -> … }
strictControl.demand.static.aStaticMethod {-> … }
使用mockFor的通用模式是:mock.demand.(static.)?method(min…max){implement}。其中:
- static在mock静态方法时使用
- min…max指定方法期望被调用的最小和最大次数,缺省是1..1,表示只调用一次
- 后面的闭包表示实现
mockFor的另一个参数是loose,表示mock出来的对象是否是严格的。缺省为false,即严格。所谓严格是指mock对象上的方法调用是有顺序的。
def looseControl = mockFor(MyService, true)
以上只是mock需要做的工作,它的结果就是我们需要的目标对象,通过在mock对象上调用createMock得到。在mock上调用verify来验证所期望的方法是否按预期方式调用。
以上内容对于使用过easyMock的读者应该不会陌生的。mockFor例子:
def otherControl = mockFor(OtherService)
otherControl.demand.newIdentifier(1..1) {->
return testId
}
//要测试的服务
def testService = new MyService()
//注入mock实例
testService.otherService = otherControl.createMock()
//调用测试方法
def retval = testService.createSomething()
因为Domain Class实在太常用了,因此Grails也提供了对它的mock支持,这就是:mockDomain(class, testInstances = ),用于模拟Domain Class:
- testInstances相当于内存“数据库”
- 模拟了CRUD和findBy*操作,这些操作依据testInstances进行
- 在进行保存时会调用validate
- 没有实现Criteria和HQL
例子:
mockDomain(Item)
def testInstances = Item.list()
由于Grails已经对Domain Class提供了CRUD动态方法,单独对这些自动产生的方法进行测试的意义不再特别大。反观我们在创建Domain Class的时候,我们最常写的就是约束。因此,个人觉得,大多数对Domain Class的测试还是主要体现在对约束的测试上。当然,如果你还给Domain Class增加了其他的方法,对这些方法的测试也是必要的,除非它们实在太简单了。
对于约束的测试,Grails提供了mockForConstraintsTests(class, testInstances = ),它们用于专门对Domain Class和Command Object进行约束测试。Grails对此只只模拟了validate方法:
mockForConstraintsTests(Book, [ existingBook ])
def book = new Book()
assertFalse book.validate()
//查找指定域,比较约束名
assertEquals "nullable", book.errors["title"]
assertEquals "nullable", book.errors["author"]
另一些常用的mock方法:
- mockLogging(class, enableDebug = false),模拟log属性
- mockController(class),模拟Controller,与ControllerUnitTestCase结合使用
- mockTagLib(class),模拟TagLib,与TagLibUnitTestCase结合使用
以上对单元测试的讨论主要集中在对于Grails Mock方法的介绍了,至于如何书写,跟写junit测试没有太大区别。接下来,我们看看集成测试。
Grails集成测试环境和运行时环境完全一样,但是使用Test环境的配置。对于request之类的对象,也是通过mock对象来完成的。request、response和session分别对应:
- MockHttpServletRequest
- MockHttpServletResponse
- MockHttpSession
需要注意的要点:
- 不会调用拦截器,使用功能测试对它们进行测试。
- 对于Controller引用的Service,需显式注入。
- 使用Request的Params构造Command对象。
一个集成测试的例子:
- Controller:
class FooController {
def text = {
render "bar"
}
def someRedirect = {
redirect(action:"bar")
}
}
- 测试:
class FooControllerTests extends GroovyTestCase {
void testText() {
def fc = new FooController()
//触发对应的action
fc.text()
assertEquals "bar",
//注意这里
fc.response.contentAsString
}
void testSomeRedirect() {
def fc = new FooController()
fc.someRedirect()
assertEquals "/foo/bar",
fc.response.redirectedUrl
}
}
测试带服务的Controller的例子:
class FilmStarsTests extends GroovyTestCase {
def popularityService //利用grails注入
public void testInjectedServiceInController () {
def fsc = new FilmStarsController()
//显式注入
fsc.popularityService = popularityService
……
}
}
对于Command Object:构造Params内容,模拟Command对象和Domain Class。
class AuthenticationController {
def signup = { SignupForm form -> …… }
}
//测试
def controller = new AuthenticationController()
controller.params.login = "marcpalmer"
controller.params.password = "secret"
controller.params.passwordConfirm = "secret"
controller.signup()
测试response内容(例子):
- Controller实例.response.contentAsString
- Controller实例.response.redirectedUrl
测试Render:
- render(view:"create", model:[book:book]):Controller实例.modelAndView.model.book,Controller实例.modelAndView.view
- [book: new Book(params['book']) ]:Controller实例.book
模拟Request,先看一下Controller的代码:
def create = {
[book: new Book(params['book']) ]
}
测试代码的总框架:
void testCreate() {
def controller = new BookController()
模拟Request //使用不同模拟方法替换这里
def model = controller.create()
assert model.book
assertEquals "The Stand", model.book.title
}
使用XML模拟的方法(注意最后调用getBytes):
controller.request.contentType = 'text/xml'
controller.request.contents = '''
<?xml version="1.0" encoding="ISO-8859-1"?>
<book>
<title>The Stand</title>
...
</book>
'''.getBytes()
使用JSON模拟(注意最后调用getBytes):
controller.request.contentType = "text/json"
//需要指定class属性,方便构造domain class
//XML通过元素名就已经隐含指定
controller.request.content =
'{"id":1,"class":"Book","title":"The Stand"}'
.getBytes()
Web Flow实现了复杂的页面流,对于它的测试,在Grails中是使用grails.test.WebFlowTestCase。其中,需要注意的方法有:
- getFlow,指定使用哪个WebFlow定义
- getFlowId,指定web flow id
- startFlow,启动web flow
- signalEvent,触发事件
看看例子:
- Web Flow定义:
class ExampleController {
def exampleFlow = {
start {
on("go") {
flow.hello = "world"
}.to "next"
}
next {
on("back").to "start"
on("go").to "end"
}
end()
}
}
- 测试例子:
class ExampleFlowTests extends grails.test.WebFlowTestCase {
def getFlow() { new ExampleController().exampleFlow }
String getFlowId() { "example" }
void testExampleFlow() {
def viewSelection = startFlow()
assertEquals "start", viewSelection.viewName
viewSelection = signalEvent("go")
assertEquals "next", viewSelection.viewName
assertEquals "world", viewSelection.model.hello
}
}
对于标签库的测试有两种测试方法:作为普通方法进行测试和作为页面元素进行测试。
- 标签库
class FooTagLib {
def bar = { attrs, body ->
out << "<p>Hello World!</p>"
}
def bodyTag = { attrs, body ->
out << "<${attrs.name}>"
out << body()
out << "</${attrs.name}>"
}
}
- 作为普通方法进行测试,基类GroovyTestCase
class FooTagLibTests extends GroovyTestCase {
void testBarTag() {
assertEquals "<p>Hello World!</p>"
, new FooTagLib().bar(null,null)
}
void testBodyTag() {
assertEquals "<p>Hello World!</p>"
, new FooTagLib().bodyTag(name:"p") {
"Hello World!"
}
}
}
- 作为页面元素进行测试,用于集成测试,基类:grails.test.GroovyPagesTestCase
class FormatTagLibTests extends GroovyPagesTestCase {
void testTag() {
def template = '<g:foo />'
assertOutputEquals( '<p>Hello World!</p>', template)
//applyTemplate可以直接获得标签库的结果
def result = applyTemplate( template)
assertEquals '<p>Hello World!</p>', result
}
}
在集成测试中,使用Domain Class时,需要注意:需要显式flush,确保持久化,否则可能无法查询。