Fork me on GitHub

1 Jan 2010

Using GMock to complement Grails mockDomain

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
  1. mock(MyDomain) establishes a partial mock of the domain class.
  2. 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).
  3. 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.

16 comments:

Johnny Jian said...

Good to see a blog introducing GMock. :)

Actually, "mock(MyDomain).static..." is called static mocking, and partial mocking is something like "mock(controller)...". Nevertheless, static mocking is also mocking static methods partially, so you can call it "static partial mocking". :)

There is a new feature called chained methods in the incoming GMock 0.9.0 can simplify the mocking the createCriteria(). You can chains the methods like "mock(MyDomain).static.createCriteria().chains().list(...).returns(results)".

What's more, you can also test the closure of withCriteria() in GMock 0.9.0 like:
mock(MyDomain).static.withCriteria(invoke {
between(...)
like(...)
}).returns(results)

Rob said...

Nice, I didn't know about that feature.

Chaining will be really useful as well.

Steve Dalton said...

Awesome post Rob, just what I needed!

Quique said...

Rob, I'm trying to follow your examples, mainly with the named queries, but in my case to test the named query itself.

So I have something like (sorry for my spanish coding):

def results = [] << Historico.findByProgramaID('2') << Historico.findByProgramaID('3') << Historico.findByProgramaID('4')

def mockCriteria = mock() {
list(instanceOf(Closure)).returns(results)
}

mock(Historico).static.todosActivos().returns(mockCriteria)

And then when I try to play it, I'm getting an error:

play {
assert results == Historico.todosActivos()
}

Could it be even possible to do something like this?:

play {
def r = Historico.todosActivos()
assert ['2', '3', '4'] == r*.programaID

Thanks in advance.

Johnny Jian said...

I think it should be

assert results == Historico.todosActivos().list {...}

Quique said...

Unfortunately I get this error when trying the "Historico.todosActivos().list { ... }:

nexpected method call 'list()' on 'Mock for Object' 'list(an instance of groovy.lang.Closure)' on 'Mock for Object': expected 1, actual 0 'Historico.todosActivos()': expected 1, actual 1

junit.framework.AssertionFailedError: Unexpected method call 'list()' on 'Mock for Object'
'list(an instance of groovy.lang.Closure)' on 'Mock for Object': expected 1, actual 0
'Historico.todosActivos()': expected 1, actual 1
at org.gmock.internal.InternalMockController.fail(InternalMockController.groovy:185)
at org.gmock.internal.InternalMockController$fail.call(Unknown Source)
at org.gmock.internal.metaclass.MetaClassHelper.findExpectation(MetaClassHelper.groovy:25)
at org.gmock.internal.metaclass.MetaClassHelper$findExpectation.callStatic(Unknown Source)
at org.gmock.internal.MockInternal.invokeMockMethod(MockInternal.groovy:80)
at org.gmock.internal.metaclass.MockProxyMetaClass$2.call(MockProxyMetaClass.java:53)
at org.gmock.internal.InternalMockController.doWork(InternalMockController.groovy:218)
at org.gmock.internal.InternalMockController.this$2$doWork(InternalMockController.groovy)
at org.gmock.internal.InternalMockController$this$2$doWork.callCurrent(Unknown Source)
at org.gmock.internal.InternalMockController$this$2$doWork.callCurrent(Unknown Source)
at org.gmock.internal.InternalMockController.doInternal(InternalMockController.groovy:207)
at org.gmock.internal.InternalMockController$doInternal.callCurrent(Unknown Source)
at org.gmock.internal.InternalMockController$doInternal.callCurrent(Unknown Source)
at org.gmock.internal.InternalMockController.doInternal(InternalMockController.groovy:200)
at org.gmock.internal.metaclass.MockProxyMetaClass.invokeMethod(MockProxyMetaClass.java:47)
at org.gmock.internal.metaclass.MockProxyMetaClass.invokeMethod(MockProxyMetaClass.java:43)
at es.cuestamenos.dominio.HistoricoTests$_testTodosActivos_closure2.doCall(HistoricoTests.groovy:83)
at es.cuestamenos.dominio.HistoricoTests$_testTodosActivos_closure2.doCall(HistoricoTests.groovy)
at org.gmock.internal.InternalMockController.play(InternalMockController.groovy:116)
at org.gmock.internal.InternalMockController$play.call(Unknown Source)
at org.gmock.GMockController.play(GMockController.groovy:29)
at org.gmock.GMockController$play.call(Unknown Source)
at es.cuestamenos.dominio.HistoricoTests.play(HistoricoTests.groovy)
at es.cuestamenos.dominio.HistoricoTests.testTodosActivos(HistoricoTests.groovy:82)

Anything else I could try?

Johnny Jian said...

I think you should use "list{}" instead of "list()"

Quique said...

That did the trick. I forgot that I defined the mock as list(instanceOf(Closure)) ;-)

Thanks!

Quique said...

Actually, I just realized that this trick is great to simulate the named query, but not to test it, as it will always pass the test, right?

What are your thoughts on this?

Rob said...

Yes, this technique is *not* suitable for testing the query. It's for testing the logic in code that uses the query without having to connect to a real database or set up real test data.

All you're doing in that code is testing the mock you just set up.

Quique said...

Rob, and any idea on how I could test the named query itself meanwhile support for mockDomain is added to Grails?

Ursula said...
This comment has been removed by the author.
Ursula said...

Thanks for the great example. It was a big help to us starting out with Grails.
If your named query takes parameters, you need to define it a bit differently.

def results=[]
def params
def mockCriteria = mock() {
list().returns( results)
}
mock(Keyword).static.myNamedQuery(params)
.returns(mockCriteria)

pedjak said...

Just a remark: mocking static methods using GMock will make tests fall if you or some Grails plugin need to call other static methods on particular class. The fix is to combine GMock with the standard Groovy metaprogramming:

def results = // whatever you want your criteria query to return
def mockCriteria = mock() {
list(instanceOf(Closure)).returns(results)
}
MyDomain.static.createCriteria = { -> mockCriteria}

play {
controller.myAction()
}

Unknown said...

Thanks for your sharing! The information your share is very useful to me and many people are looking for them just like me! thank you! I hope you have many useful articles to share with everyone!
slither io

صيانة اريستون said...

كل سنه وانتم بخير اليكم بعض المواقع الهامه لكل من لديها جهاز بيه اى عطل
صيانة غسالات هيتاشى
صيانة هيتاشى
مبيعات يونيون اير
تكييف يونيون اير
صيانة اوليمبيك
صيانة زانوسي
صيانة سامسونج
صيانة توشيبا
صيانة غسالات زانوسي
صيانة غسالات سامسونج
صيانة ثلاجات كريازي
صيانة ثلاجات توشيبا
صيانة سخانات اوليمبيك موقع
صيانة ثلاجات زانوسي موقع
صيانة سامسونج موقع
صيانة توشيبا شركة
صيانة غسالات توشيبا موقع
صيانة ثلاجات توشيبا موقع
صيانة كريازي موقع
صيانة غسالات اوليمبيك موقع
الموقع الرسمي
صيانة سيمنس موقع
صيانة سخانات سيمنس
صيانة غسالات كريازي
رقم صيانة سيمنس
اسعار تكييف يونيون اير موقع