Test pollution (part 2)

In my previous post I discussed test pollution in Grails running integration tests when exercising the metaClass of a class instance.

This post will deal with another thing I noticed in the codebase: Mocking injected dependencies on a bean under test. An example could be a testService that has anotherService injected:

class TestService {
    AnotherService anotherService

    String fromAnotherService() {
        anotherService.anotherMethod()
    }
}
class AnotherService {
    String anotherMethod() {
        return "Another Method"
    }
}

Remember the bean wiring is done by Grails.

The integration test could look like this:

@Integration
class MockAserviceSpec extends Specification {

    TestService testService // Autowired by Grails

    def "test before mocking"() {
        expect:
            testService.fromAnotherService() == 'Another Method'
    }

    def "stubbing anotherService on testService"() {
        given:
            testService.anotherService = Stub(AnotherService) {
                anotherMethod() >> 'Mocked Method'
            }
        expect:
            testService.fromAnotherService() == 'Mocked Method'
    }

    def "test after mocking"()  {
        expect:
            testService.fromAnotherService() == 'Another Method'

    }
}

The problem is, that when replacing anotherService with a Stub (or Mock or Spy), testService keeps this change in subsequent test cases. In this case, the last test (“test after mocking”) will fail, and this is the result:

testService.fromAnotherService() == 'Another Method'
|           |                    |
|           ""                   false
test.TestService@5b087a6b        14 differences (0% similarity)
                                 (--------------)
                                 (Another Method)

So not only is our test polluted, but but Spock resets the Stub when running the next test case, thus making the result even more unpredictable.

So just like the MockMetaClassHelper I came up with a similar solution to the problem:

trait MockServiceHelper {
    private Map<object>> replacedServices = [:].withDefault { [] as Set }
    private log = LoggerFactory.getLogger(this.getClass().name)

    boolean postponeServiceCleanup = false

    /**
     * Replace a service injected instances with another bean and save the variable name
     * which was touched
     * @param service
     * @param variable The variable replaced
     * @param the replacement instance (mock or another bean)
     * @return
     */
    Object replaceService(Object service, String variable, Object replacement) {
        Set variables = replacedServices.get(service)

        variables << variable

        service[variable] = replacement

        log.debug "Replaced ${service.getClass().name}.${variable} with ${replacement.getClass()?.name ?: 'a proxy'}"
        return replacement
    }
    /**
     * Restore the injected variable with the bean in Application Context. If it was not replaced, nothing happens
     * @param instance
     */
    void restoreServiceVariable(Object service, String variable) {

        Set<String> variables = replacedServices.get(service)
        if (variables.contains(variable)) {
            internalRestoreServiceBean(service, variable)
        }
    }

    /**
     * Automatically restores all service variable changes after a test finishes (cleanup)
     */
    @After
    void cleanupServices() {
        if (!postponeServiceCleanup) {
            replacedServices.each { service, variables ->
                variables.each { variable ->
                    internalRestoreServiceBean(service, variable)
                }
            }
            replacedServices.clear()
        }
    }

    /**
     * Automatically restores all service variable changes after a test finishes (cleanupSpec)
     */
    @AfterClass
    void cleanupSpecServices() {
        if (postponeServiceCleanup) {
            replacedServices.each { service, variables ->
                variables.each { variable ->
                    internalRestoreServiceBean(service, variable)
                }
            }
            replacedServices.clear()
        }
    }

    private void internalRestoreServiceBean(Object service, String variable) {
        Object originalBean = Holders.applicationContext.getBean(variable)

        // Using that autowireByName is used in Grails
        service[variable] = originalBean
        log.debug "Restored ${service.getClass().name}.${variable} with ${originalBean.getClass().name}"
    }

}

Now the specification can be written like this:

@Integration
class MockAserviceSpec extends Specification implements MockServiceHelper {

    TestService testService // Autowired by Grails

    def "test before mocking"() {
        expect:
            testService.fromAnotherService() == 'Another Method'
    }

    def "stubbing anotherService on testService"() {
        given:
            replaceService(testService,'anotherService', Stub(AnotherService) {
                anotherMethod() >> 'Mocked Method'
            })
        expect:
            testService.fromAnotherService() == 'Mocked Method'
    }

    def "test after mocking"()  {
        expect:
            testService.fromAnotherService() == 'Another Method'

    }
}

The only change is, that we let the MockServiceHelper replace anotherService on testService, and with the annotation @After we let the trait cleanup when the test is done, by re-wiring the beans with their original bean reference from applicationContext, thus making the test greeen.

This makes it easier to replace a beans injected dependencies and restore them to their original value when a test has completed.

I hope you find it useful. Please leave a comment in any case.

Stay Groovy!

 
Blog comments powered by Disqus