在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,确保持久化,否则可能无法查询。
转载于:https://blog.51cto.com/bcptdtptp/306635