在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,确保持久化,否则可能无法查询。