Since Grails 1.1 we've had pretty good
unit testing support via
GrailsUnitTestCase and its sub-classes. The
mockDomain method is particularly useful for simulating the various enhancements Grails adds to domain classes. However, there are some domain class capabilities, such as criteria queries and the new
named queries, that can't really be simulated by
mockDomain.
So assuming we're trying to unit test a controller that uses criteria methods or named queries on a domain class how can we enhance the capabilities of
mockDomain? One of my favourite Groovy libraries is
GMock which I use in preference to Groovy's built in mock capabilities. One of its really powerful features is the ability to use
'partial mocks',
i.e. to mock particular methods on a class whilst allowing the rest of the class to continue functioning as normal. This means we can layer a mocked
createCriteria,
withCriteria or named query call on to a domain class that is already enhanced by
mockDomain.
First off you need to add the GMock dependency to your
BuildConfig.groovy. Since GMock supports
Hamcrest matchers for matching method arguments you'll probably want those as well:
dependencies {
test "org.gmock:gmock:0.8.0"
test "org.hamcrest:hamcrest-all:1.0"
}
If you're using an earlier version of Grails you'll need to just grab the jar files and put them in your app's
lib directory.
Then in your test case you need to import GMock and Hamcrest classes and add an annotation to allow GMock to work:
import grails.test.*
import org.gmock.*
import static org.hamcrest.Matchers.*
@WithGMock
class MyControllerTests extends ControllerUnitTestCase {
Adding criteria and named query methods is now fairly simple:
Mocking a withCriteria method
def results = // whatever you want your criteria query to return
mock(MyDomain).static.withCriteria(instanceOf(Closure)).returns(results)
play {
controller.myAction()
}
Breaking this example down a little
mock(MyDomain)
establishes a partial mock of the domain class.
instanceOf(Closure)
uses a Hamcrest instanceOf matcher to assert that the withCriteria method is called with a single Closure argument (the bit with all the criteria in).
returns(results)
tells the mock to return the specified results which here would be a list of domain object instances.
In this example we're expecting the
withCriteria method to be called just once but GMock supports more complex
time matching expressions if the method may be called again.
Mocking a createCriteria method
The
withCriteria method returns results directly but
createCriteria is a little more complicated in that it returns a criteria object that has methods such as
list,
count and
get. To simulate this we'll need to have the mocked
createCriteria method return a mocked criteria object.
def results = // whatever you want your criteria query to return
def mockCriteria = mock() {
list(instanceOf(Closure)).returns(results)
}
mock(MyDomain).static.createCriteria().returns(mockCriteria)
play {
controller.myAction()
}
This is only a little more complex than the previous example in that it has the mocked
list method on another mock object that is returned by the domain class'
createCriteria method.
mock()
provides an un-typed mock object as we really don't care about the type here.
Mocking a named query
For our purposes named queries are actually pretty similar to
createCriteria.
def results = // whatever you want your criteria query to return
def mockCriteria = mock() {
list(instanceOf(Closure)).returns(results)
}
mock(MyDomain).static.myNamedQuery().returns(mockCriteria)
play {
controller.myAction()
}
Some other examples:
Mocking a named query with an argument
mock(MyDomain).static.myNamedQuery("blah").returns(mockCriteria)
For simple parameters you don't need to use a Hamcrest matcher - a literal is just fine.
Mocking a withCriteria call using options
You can pass an argument map to
withCriteria,
e.g. withCriteria(uniqueResult: true) { /* criteria */ }
will return a single instance rather than a
List. To mock this you will need to expect the
Map as well as the
Closure:
def result = // a single domain object instance
mock(MyDomain).static.withCriteria(uniqueResult: true, instanceOf(Closure)).returns(result)
Mocking criteria that are re-used
It's fairly common in pagination scenarios to call a
list and
count method on a criteria object. We can just set multiple expectations on the mock criteria object, e.g.
def mockCriteria = mock() {
list(max: 10).returns(results)
count().returns(999)
}
mock(MyDomain).static.myNamedQuery().returns(mockCriteria)
The nice thing about this technique is that it doesn't interfere with any of the enhancements
mockDomain makes to the domain class, so the
save,
validate, etc. methods will still work as will dynamic finders.
Be aware however, that what we're doing here is
mocking out the criteria queries, not testing them! All the interesting stuff inside the criteria closure is being ignored by the mocks and could, of course, be garbage. Named queries are pretty easy to test by having integration test cases for your domain class. Criteria queries beyond a trivial level of complexity should really be encapsulated in service methods or named queries and integration tested there. Of course, GMock then makes an excellent solution for mocking that service method in your controller unit test.