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!