It is easy to use the Spock Framework, shipped with Grails, to mock or stub a collaborator (such as a service) in our Grails unit tests.
The Testing chapter explains a bit about mocking collaborators, doWithSpring
/doWithConfig
callback methods, the FreshRuntime
annotation to mock beans in tests — but they’re mainly for unit testing.
How about mocking beans in an integration test?
Example
What if we have a controller
class AnimalRegistrationController { AnimalRegistrationService animalRegistrationService def arrival(ArrivalCommand arrival) { animalRegistrationService .registerArrival(arrival) .map { ArrivalErrorMessage aem -> renderErrors(aem) }.orElse { render status: 200 } } }
which calls a service, which calls a repository – which might do external calls which you don’t want to happen in an integration test.
class AnimalRegistrationService { ArrivalRepository arrivalRepository Optional registerArrival(Arrival arrival) { arrivalRepository.registerArrival(arrival) } }
Previously I wrote that Grails 3.3 has Spock 1.1 — which gave us a few new features to use such as a default answer for java.util.Optional
…but it gave us more!
1. DetachedMockFactory and TestConfiguration
Now we also have a DetachedMockFactory we can use to declare mocks outside the hierarchy of a outside of a Specification
, e.g. in a Spring configuration.
I got triggered by this article about Spring Integration testing, and I adjusted it to work for Grails 3.3 — which is based on Spring Boot but doesn’t quite use all the Spring annotations we’re used to in a vanilla Spring application.
So we create a configuration, specifically for testing, in src/test/groovy
using a DetachedMockFactory
like
import spock.mock.DetachedMockFactory import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean ... @TestConfiguration class TestRepositoryConfig { private DetachedMockFactory factory = new DetachedMockFactory() @Bean ArrivalRepository arrivalRepository() { factory.Mock(ArrivalRepository) } }
2. Integration test
We can now use the mocked bean in our Grails 3 integration test, by injecting it by type using @Autowired
. We can create the expectations as usual.
@Integration class ArrivalApiIntegrationSpec extends Specification { @Value('${local.server.port}') Integer serverPort @Autowired ArrivalRepository mockedArrivalRepository void "should create an arrival"() { given: 1 * mockedArrivalRepository.registerArrival(_) >> { Optional.empty() } when: def response = new RestBuilder().post('http://localhost:{serverPort}/api/arrivals') { urlVariables([serverPort: serverPort]) json { animalId = 1 date = '2017-01-01' } } then: response.status == 200 } }
2. Dependency
For the above to work, you actually have to pull in one essential spock-lang dependency.
Add it to your build.gradle
dependencies { ... testCompile 'org.spockframework:spock-core:1.1-groovy-2.4' testCompile 'org.spockframework:spock-spring:1.1-groovy-2.4'
Bada-bing. It’s done.
That’s it
We have now full control over our mocked beans, as if we were in a unit test.
Hello Ted,
If I understood correctly. With the configuration you showed, ArrivalRepository will be mocked for integration/functional tests. What if you only wanted to mock it for a single test ArrivalApiIntegrationSpec but use the real implementation in other integration tests.
thank,
Sergio del Amo
LikeLike
Hi Sergio,
I haven’t been able to have Grails’ @Integration-annotated test use a specific TestConfiguration per test — in order to say what test uses the real beans and which should get mocks. The tip of Matthijs (see other comment) could work.
Thx, Sergio.
LikeLike
Hi Sergio,
If you only want to override it for a single test, then simply only mock it in that test. You can inject the real AnimalRegistrationService, override the arrivalRepository with animalRegistrationService.arrivalService = Mock(ArrivalRepository). Don’t forget to clean up and “restore” the original arrivalRepository back to it’s original value in the cleanup() method. An easy way to get the original is to inject it into your test as well.
LikeLiked by 1 person
Hi Matthijs,
I guess that would work indeed. Thanks for your feedback!
Regards, Ted
LikeLike