Using Mock Annotation and mockFor in Grails Unit Tests

More and more often, when reading documentation, I find myself thinking of the lyrics from The Kinks song Lola:

… well I’m not dumb, but I can’t understand…

Too often, I find the documentation to be sparse and the examples included too simple. A gap exists from reading the documentation, looking at the included examples and transposing that unto real-world problems that are not quite that simple.

My current project at work is using Grails 2.0.x and I was very glad to see that it included a whole collection of unit testing mixins. In particular, there were three things I wanted to start using right away when testing a service:

  • TestFor annotation
  • Mock annotation
  • Defining mocks and stubs programtically

Now although that the documentation is indeed sparse, it turns out that it actually is very concise. The documentation states that:

Most testing can be achieved via the TestFor annotation in combination with the Mock annotation for mocking collaborators

@Mock([Book, Author, BookService])

followed by

The Mock@ annotation creates mock version of any collaborators. There is an in-memory implementation of GORM that will simulate most interactions with the GORM API. For those interactions that are not automatically mocked you can use the built in support for defining mocks and stubs programmatically. For example:

def control = mockFor(SearchService)
control.demand.searchWeb { String q -> ['mock results'] }
control.demand.static.logResults { List results ->  }
controller.searchService = control.createMock()

I first really understood what it said after playing around with it for a while. It is here that some real-life examples along with a few words of explanation would have been very helpful. So here goes with an example from the real world – hopefully this can get you going full speed with unit test mixins a little faster than I could.

I have a Service that receives an event that a piece of musik has been played on one of our radio stations. First I need to retrieve the channel, determine which program played it and finally call a rest-service that returns all that we know about this particular piece of musik, and then persist this particular track played.

So the service I need to test, looks like this when stripped down to its essence:

class TrackPlayedEventService {
  def musicService

  void handleTrackPlayedEvent(TrackPlayedEvent event) {
    def channel = Channel.findByName(event.channelName)
    def program = Program.findByChannelAndStartTimeLessThanOrEqualsAndEndTimeGreaterThanOrEquals(
        channel, event.startTime, event.endTime)
    def musikInfo = musicService.getTrackInfo(event.trackId)
    def trackPlayed = new TrackPlayed(trackId: event.trackId, program: program, 
        salesArtists: musikInfo.salesArtists, trackTitle: musikInfo.title)

Now in the above example, I need to have mock versions of Channel and Program as well as the resulting object of type TrackPlayed with basic GORM functionality, and to stub the musicService.getTrackInfo(trackId) method. Therefore, the Mock annotation is perfect for Channel, Program, and TrackPlayed, but for the MusikService I need to use the mockFor() method, as I need some real data returned packaged in a MusikInfo object.

@Mock([Channel, Program, TrackPlayed])
class TrackPlayedEventServiceTests {
  def eventObject
  void setup() {
    eventObject = new TrackPlayedEvent(channelName: 'P3', trackId: '1234567-1-1', startTime:
        Date.parse('yyyy-MM-dd HH:mm:ss', '2012-03-23 11:23:08'), 
        endTime: Date.parse('yyyy-MM-dd HH:mm:ss', '2012-03-23 11:27:46'))
    // Use the Channel and Program mocks created by @Mock annotation to create a channel and program
    def myChannel = new Channel(name: 'P3') false)
    new Program(channel: myChannel, startTime: Date.parse('yyyy-MM-dd HH:mm:ss', '2012-03-23 11:00:00'),
        endTime: Date.parse('yyyy-MM-dd HH:mm:ss', '2012-03-23 13:00:00')).save(validate: false)

  void testHandleTrackPlayedEvent() {
    // create mockFor service and stub the method that is to be intercepted
    def mockService = mockFor(MusicService)
    // NOTE: if the method you're mocking has zero arguments remember to start your closure with {->
    // else it will complain because of the implicit it argument to a closure
    mockService.demand.getTrackInfo {trackId ->
      return getMusikInfoObject()
    service.musicService = mockService.createMock()

    // Using the TrackPlayed mock - find (hopefully) the TrackPlayed persisted by the service method under test
    def trackPlayed = TrackPlayed.findByTrackId('1234567-1-1')
    assert trackPlayed 
    assert trackPlayed.title == 'My Song Title'

  MusikInfo getMusikInfoObject()
    return new MusikInfo(trackId: '1234567-1-1', title: 'My Song Title', salesArtists: ['Artist one', 'Artist two'])

Hopefully this has been a help to you, which means that I am not alone in taking my time to fully understand the available documentation. But in case it is only me that is slow on the uptake, I’ll take comfort in the saying that ignorance is bliss.