Question

Within a Spock unit test, I am trying to test the behaviour of a method findRepositoriesByUsername independent of getGithubUrlForPath, both belonging to the same service.

Repeated attempts to use the metaClass have failed:

  • String.metaClass.blarg produces an error No such property: blarg for class: java.lang.String
  • service.metaClass.getGithubUrlForPath to modify the service instance doesn't work
  • GithubService.metaClass.getGithubUrlForPath to modify the service class doesn't work
  • Tried adding/modifying methods on the metaClass in the test methods' setup and when blocks, neither worked as expected

The test:

package grails.woot

import grails.test.mixin.TestFor

@TestFor(GithubService)
class GithubServiceSpec extends spock.lang.Specification {

    def 'metaClass test'() {
        when:
        String.metaClass.blarg = { -> 
            'brainf***'
        }

        then:
        'some string'.blarg == 'brainf***'
    }

    def 'can find repositories for the given username'() {
        given:
        def username = 'username'
        def requestPathParts

        when: 'the service is called to retrieve JSON'
        service.metaClass.getGithubUrlForPath = { pathParts ->
            requestPathParts = pathParts
        }
        service.findRepositoriesByUsername(username)

        then: 'the correct path parts are used'
        requestPathParts == ['users', username, 'repos']
    }

}

The service:

package grails.woot

import grails.converters.JSON

class GithubService {

    def apiHost = 'https://api.github.com/'

    def findRepositoriesByUsername(username) {
        try{
            JSON.parse(getGithubUrlForPath('users', username, 'repos').text)
        } catch (FileNotFoundException ex) {
            // user not found
        }
    }

    def getGithubUrlForPath(String ... pathParts) {
        "${apiHost}${pathParts.join('/')}".toURL()
    }
}

I've tested the String.metaClass.blarg example in the groovy shell (launched by grails), and it did as expected.

Do I have a fundamental misunderstanding here? What am I doing wrong? Is there a better way to handle the desired test (replacing a method on the service under test)?

Was it helpful?

Solution

This is how the tests can be written to make them pass:

def 'metaClass test'() {
    given:
        String.metaClass.blarg = { -> 'brainf***' }

    expect:
        // note blarg is a method on String metaClass 
        // not a field, invoke the method
        'some string'.blarg() == 'brainf***'
}

def 'can find repositories for the given username'() {
    given:
        def username = 'username'
        def requestPathParts

    when: 'the service is called to retrieve JSON'
        service.metaClass.getGithubUrlForPath = { String... pathParts ->
            requestPathParts = pathParts
            [text: 'blah'] // mimicing URL class
        }
        service.findRepositoriesByUsername(username)

    then: 'the correct path parts are used'
        requestPathParts == ['users', username, 'repos']
}

OTHER TIPS

Why don't you use Spock's great Mocking abilities?

Look at http://spockframework.github.io/spock/docs/1.0/interaction_based_testing.html#_creating_mock_objects

There is no need to peek inside metaclass itself, you can create some stub object, and demanded method will be called instead of original one. Also you can use Groovy's MockFor and StubFor, they can be a little bit easier.

You cannot fully trust metaclass inside spock tests specification.

  1. There is some complex logic inside it, which can easyly mess thing's up. Try run some tests under debugger, and you will see it.
  2. Spock uses metaclasses under the hood. It can override your own try's.
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top