Lets consider an example. I want to write some test coverage for a controller that looks up a domain object based on a parameter and places it in the model. Usual disclaimer: of course, in the real world I'd have written the test first, etc. Nothing much to the controller:
class LolrusController {and here's the domain class:
def show = {
def myLolrus = Lolrus.findByName(params.lolrusName)
render(view: 'lolrus', model: [lolrus: myLolrus])
}
}
class Lolrus {Okay, so a reasonable test (ignoring edge cases for now) could be to create a couple of domain objects, make a request to the controller and verify that the correct domain object is retrieved. Simple, right?:
String name
String mood
static hasMany = [posessions: Item]
static constraints = {
name(unique: true)
posessions(validator: { posessions ->
return posessions.any { it.type == 'bukkit' }
})
}
String toString() { "Lolrus[$name]" }
boolean equals(Object o) { return o instanceof Lolrus && o.name == name }
int hashCode() { return 37 * name.hashCode() }
}
class LolrusControllerTests extends GroovyTestCase {Unfortunately, not so simple. The test fails and reports
def controller
def lolrus1, lolrus2
void setUp() {
controller = new LolrusController()
lolrus1 = new Lolrus(name: 'Hugh')
lolrus2 = new Lolrus(name: 'Alan')
[lolrus1, lolrus2]*.save(flush: true)
}
void testShowActionFindsCorrectLolrusAndSticksItInModel() {
controller.params.lolrusName = lolrus1.name
def model = controller.show()
assertEquals('/lolrus/show', controller.modelAndView.viewName)
assertEquals(lolrus1, controller.modelAndView.model.lolrus)
}
}
expected:<Lolrus[Hugh]> but was:<null>What we've forgotten of course is that Lolrus has a mandatory field mood and a custom validation on its posessions association that requires it to have a bukkit. The two Lolrus instances we tried to create in setUp couldn't be saved. Great. Well, let's just add that to the test set up:
void setUp() {Okay, it works but look how much of what's going on in setUp is actually nothing to do with anything the test cares about. This is a very simple example with a single test case. Imagine how much worse that setUp method is going to get when we add more tests or the Lolrus domain object gets more complex... Imagine the refactoring required when the Lolrus domain object acquires other constraints... Imagine the query is more complex requiring more domain objects to be set up to really prove it works...
controller = new LolrusController()
lolrus1 = new Lolrus(name: 'Hugh', mood: 'worryingly cheerful')
lolrus1.addToPosessions new Item(type: 'bukkit')
lolrus2 = new Lolrus(name: 'Alan', mood: 'mildly distressed')
lolrus2.addToPosessions new Item(type: 'bukkit')
[lolrus1, lolrus2].each {
assert it.save(flush: true), it.errors
}
}
Okay, we could mock the Lolrus class, but the code for that isn't much cleaner and alarm bells start ringing when you start using mocks in an integration test. Shouldn't this be a unit test? Sure, that would be nice but we're using the render dynamic method in our controller, we'll have to mock that out on the metaClass, mock out the findByName method on Lolrus, etc. etc. Too much hassle right? Not any more.
grails install-plugin testingAllows us to (among other things) write controller unit test cases like this:
class LolrusControllerUnitTests extends grails.test.ControllerUnitTestCase {Much neater. We're not worrying about the constraints required to set up valid Lolrus instances, but neither are we having to mock out the findByName method.
def controller
def lolrus1, lolrus2
void setUp() {
super.setUp()
controller = new LolrusController()
lolrus1 = new Lolrus(name: 'Hugh')
lolrus2 = new Lolrus(name: 'Alan')
mockDomain(Lolrus, [lolrus1, lolrus2])
}
void testShowActionFindsCorrectLolrusAndSticksItInModel() {
controller.params.lolrusName = lolrus1.name
controller.show()
assertEquals('show', renderArgs.view)
assertEquals(lolrus1, renderArgs.model.lolrus)
}
}
The mockDomain method allows you to set up some domain objects for which all the usual Grails domain class dynamic methods will work - including findBy..., addTo..., etc. The collection passed as the second argument to mockDomain defines all the instances that exist and they can be retrieved just as a real domain object could. The mockDomain method co-exists happily with regular Groovy mocks and the testing plugin also provides some lighter mocking capabilities via its own mockFor method.
Additionally the controller's render method and params property work without any work on our part. This is true of all the dynamic properties and methods of the controller, request, response, params, controllerName, redirect, etc.
Not only that, but this test runs faster than the integration test as it's not configuring the Spring container or an in-memory database. That speed difference only grows as the project becomes more complex - it's a couple of seconds between the two tests here, but in a more complex project it's a lot more.
There are a couple of gotchas (the plugin isn't fully mature). First off you need to remember to call super.setUp() in order that all the Grails stuff happens. The ControllerUnitTestCase class will figure out based on the class naming convention which controller class it needs to mock up. Secondly, the domain class get method doesn't seem to work with non-standard identifier types at the moment (i.e. anything other than a Long). However, this unit testing support is intended to get rolled into Grails 1.1 so it should improve rapidly.
This is only part of what the testing plugin can do. Support for unit testing domain class constraints, etc. is also included. I've also been working on a TagLibUnitTestCase that works along the same lines as ControllerUnitTestCase.
1 comment:
We've just added a bindData method to our ControllerTestCase as it's not available out of the box with the testing plugin. Code like so:
void setUp() {
super.setUp()
controller = new MyLovelyController()
controller.metaClass.bindData = { target, params ->
params.each { key, value ->
controller.myBean."$key" = value
}
}
}
Apologies for the code formatting. This comment entry window is really crap and doesn't allow pre, code or any such tags :(
Post a Comment