From c148a621508ea4807041cfe6cd5753e497553435 Mon Sep 17 00:00:00 2001 From: JeffSheets Date: Thu, 17 May 2018 15:53:48 -0500 Subject: [PATCH 1/8] feat: Unwrap Cglib Proxied @Validated service also brings in Spock 1.2-SNAPSHOT @SpringSpy @UnwrapAopProxy --- .gitignore | 1 + build.gradle | 11 ++++-- .../services/ExternalRankingService.groovy | 2 + .../PersonControllerIntTest.groovy | 38 ++++++------------- 4 files changed, 23 insertions(+), 29 deletions(-) diff --git a/.gitignore b/.gitignore index b6d6629..7d2d440 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .gradle /build/ !gradle/wrapper/gradle-wrapper.jar +out ### NPM ### /npm-debug.log diff --git a/build.gradle b/build.gradle index c1cc889..3fd9e9b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - springBootVersion = '1.5.2.RELEASE' + springBootVersion = '1.5.13.RELEASE' } repositories { mavenCentral() @@ -21,6 +21,11 @@ sourceCompatibility = 1.8 repositories { jcenter() + + maven { + //Required until Spock 1.2 is released out of snapshot + url 'https://oss.sonatype.org/content/repositories/snapshots/' + } } @@ -30,12 +35,12 @@ dependencies { compile('org.codehaus.groovy:groovy') runtime('com.h2database:h2') - def spockVersion = '1.1-groovy-2.4-rc-4' + def spockVersion = '1.2-groovy-2.4-SNAPSHOT' testCompile("org.springframework.boot:spring-boot-starter-test") testCompile("org.spockframework:spock-core:$spockVersion") testCompile("org.spockframework:spock-spring:$spockVersion") testCompile("com.blogspot.toomuchcoding:spock-subjects-collaborators-extension:1.2.1") // needed for mocking in Spock - testCompile("cglib:cglib-nodep:2.2") + testCompile("cglib:cglib-nodep:3.2.1") } diff --git a/src/main/groovy/com/objectpartners/eskens/services/ExternalRankingService.groovy b/src/main/groovy/com/objectpartners/eskens/services/ExternalRankingService.groovy index 7922e30..a8a6376 100644 --- a/src/main/groovy/com/objectpartners/eskens/services/ExternalRankingService.groovy +++ b/src/main/groovy/com/objectpartners/eskens/services/ExternalRankingService.groovy @@ -2,12 +2,14 @@ package com.objectpartners.eskens.services import com.objectpartners.eskens.entities.Person import org.springframework.stereotype.Service +import org.springframework.validation.annotation.Validated /** * This is to mimic calls to an external 3rd party service that you wouldn't want to test locally. * Created by derek on 4/10/17. */ @Service +@Validated class ExternalRankingService { @SuppressWarnings("GrMethodMayBeStatic") diff --git a/src/test/groovy/com/objectpartners/eskens/controllers/PersonControllerIntTest.groovy b/src/test/groovy/com/objectpartners/eskens/controllers/PersonControllerIntTest.groovy index 0242b4c..6cc1ff3 100644 --- a/src/test/groovy/com/objectpartners/eskens/controllers/PersonControllerIntTest.groovy +++ b/src/test/groovy/com/objectpartners/eskens/controllers/PersonControllerIntTest.groovy @@ -1,13 +1,12 @@ package com.objectpartners.eskens.controllers -import com.objectpartners.eskens.config.IntegrationTestMockingConfig -import com.objectpartners.eskens.entities.Person import com.objectpartners.eskens.services.ExternalRankingService import com.objectpartners.eskens.services.Rank +import org.spockframework.spring.SpringSpy +import org.spockframework.spring.UnwrapAopProxy import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.context.SpringBootTest -import org.springframework.context.annotation.Import import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.MvcResult import spock.lang.Specification @@ -23,15 +22,21 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. */ @SpringBootTest @AutoConfigureMockMvc -@Import([IntegrationTestMockingConfig]) //See additional notes at the bottom +//@Import([IntegrationTestMockingConfig]) //uncomment to use cached test mock config class PersonControllerIntTest extends Specification { @Autowired MockMvc mvc /** - * This is our mock we created in our test config. We inject it in so we can control it in our specs. + * SpringSpy will wrap the Spring injected Service with a Spy + * UnwrapAopProxy will remove the cglib @Validated proxy annotated inside ExternalRankingService + * However it will not use a cached test config, so many tests could be slow. + * For speed, you could just Autowire here and manually unwrap the proxy with: + * AopTestUtils.getUltimateTargetObject(externalRankingService) */ - @Autowired ExternalRankingService externalRankingServiceMock + @SpringSpy + @UnwrapAopProxy + ExternalRankingService externalRankingService def "GetRank"() { when: 'Calling getRank for a known seed data entity' @@ -39,7 +44,7 @@ class PersonControllerIntTest extends Specification { .andExpect(status().is2xxSuccessful()).andReturn() then: 'we define the mock for JUST the external service' - externalRankingServiceMock.getRank(_) >> { + 1 * externalRankingService.getRank(_) >> { new Rank(level: 1, classification: 'Captain') } noExceptionThrown() @@ -50,23 +55,4 @@ class PersonControllerIntTest extends Specification { then: 'the result contains a mix of mocked service data and actual wired component data' resultingJson == 'Capt James Kirk ~ Captain:Level 1' } - - - /* - We could define our test configuration here, but if we have multiple integration tests - and we want to mock the same things, then it's better to share the configuration for context caching, - thus the import of IntegrationTestMockingConfig - */ - - /* - @TestConfiguration - static class Config { - private DetachedMockFactory factory = new DetachedMockFactory() - - @Bean - ExternalRankingService externalRankingService() { - factory.Mock(ExternalRankingService) - } - } - */ } From cbf16ec466a57cdcfb1b115980bfd5ba2504d5df Mon Sep 17 00:00:00 2001 From: JeffSheets Date: Wed, 30 May 2018 16:59:23 -0500 Subject: [PATCH 2/8] feat: separating out examples for new spock annotations and proxy unwrapping --- README.md | 4 ++ .../services/ExternalRankingService.groovy | 3 +- .../PersonControllerIntCachedTest.groovy | 62 +++++++++++++++++++ .../PersonControllerIntTest.groovy | 5 +- 4 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 src/test/groovy/com/objectpartners/eskens/controllers/PersonControllerIntCachedTest.groovy diff --git a/README.md b/README.md index 820a249..8fbab56 100644 --- a/README.md +++ b/README.md @@ -10,5 +10,9 @@ Thankfully Spock 1.1 introduced the `DetachedMockFactory`. This, combined with t The heart of this example lives in our `PersonControllerIntTest`. The `PersonControllerIntTest` spins up a Spring context so we can make a `MockMvc` call to a REST endpoint which pulls data from an h2 database via a Spring Data repo, but the "Rank" data we would normally get from an external service has been mocked. +Updates: +* Spock 1.2 provides new @SpringSpy and @SpringBean annotations to make injecting mocks even easier. +* Mocking Spring proxied objects, like @Validated or @Repository, requires unwrapping the proxy to use the mock objects + Testing provided by Travis CI [![Build Status](https://travis-ci.org/snekse/spring-spock-integration-testing.svg?branch=master)](https://travis-ci.org/snekse/spring-spock-integration-testing) \ No newline at end of file diff --git a/src/main/groovy/com/objectpartners/eskens/services/ExternalRankingService.groovy b/src/main/groovy/com/objectpartners/eskens/services/ExternalRankingService.groovy index a8a6376..55b8822 100644 --- a/src/main/groovy/com/objectpartners/eskens/services/ExternalRankingService.groovy +++ b/src/main/groovy/com/objectpartners/eskens/services/ExternalRankingService.groovy @@ -6,10 +6,9 @@ import org.springframework.validation.annotation.Validated /** * This is to mimic calls to an external 3rd party service that you wouldn't want to test locally. - * Created by derek on 4/10/17. */ @Service -@Validated +@Validated //This makes it a spring proxied service, so unwrapping is necessary to use a Mock class ExternalRankingService { @SuppressWarnings("GrMethodMayBeStatic") diff --git a/src/test/groovy/com/objectpartners/eskens/controllers/PersonControllerIntCachedTest.groovy b/src/test/groovy/com/objectpartners/eskens/controllers/PersonControllerIntCachedTest.groovy new file mode 100644 index 0000000..0ec6a80 --- /dev/null +++ b/src/test/groovy/com/objectpartners/eskens/controllers/PersonControllerIntCachedTest.groovy @@ -0,0 +1,62 @@ +package com.objectpartners.eskens.controllers + +import com.objectpartners.eskens.config.IntegrationTestMockingConfig +import com.objectpartners.eskens.services.ExternalRankingService +import com.objectpartners.eskens.services.Rank +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.annotation.Import +import org.springframework.test.util.AopTestUtils +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.MvcResult +import spock.lang.Specification + +import static org.springframework.http.MediaType.APPLICATION_JSON +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +/** + * An integration test illustrating how to wire everything w/ Spring, + * but replace certain components with Spock mocks + */ +@SpringBootTest +@AutoConfigureMockMvc +@Import([IntegrationTestMockingConfig]) //uses a cached spring context for speed +class PersonControllerIntCachedTest extends Specification { + + @Autowired MockMvc mvc + + @Autowired + ExternalRankingService proxiedExternalRankingService + + ExternalRankingService externalRankingService + + void setup() { + /** + * the Validated proxied ExternalRankingService must be unwrapped to use the Mock + * + * see PersonControllerIntTest to see how to make this easier with @SpringSpy @UnwrapAopProxy + */ + externalRankingService = AopTestUtils.getUltimateTargetObject(proxiedExternalRankingService) + } + + def "GetRank"() { + + when: 'Calling getRank for a known seed data entity' + MvcResult mvcResult = mvc.perform(get("/persons/1/rank").contentType(APPLICATION_JSON)) + .andExpect(status().is2xxSuccessful()).andReturn() + + then: 'we define the mock for JUST the external service' + 1 * externalRankingService.getRank(_) >> { + new Rank(level: 1, classification: 'Captain') + } + noExceptionThrown() + + when: 'inspecting the contents' + def resultingJson = mvcResult.response.contentAsString + + then: 'the result contains a mix of mocked service data and actual wired component data' + resultingJson == 'Capt James Kirk ~ Captain:Level 1' + } +} diff --git a/src/test/groovy/com/objectpartners/eskens/controllers/PersonControllerIntTest.groovy b/src/test/groovy/com/objectpartners/eskens/controllers/PersonControllerIntTest.groovy index 6cc1ff3..b2c324e 100644 --- a/src/test/groovy/com/objectpartners/eskens/controllers/PersonControllerIntTest.groovy +++ b/src/test/groovy/com/objectpartners/eskens/controllers/PersonControllerIntTest.groovy @@ -22,7 +22,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. */ @SpringBootTest @AutoConfigureMockMvc -//@Import([IntegrationTestMockingConfig]) //uncomment to use cached test mock config class PersonControllerIntTest extends Specification { @Autowired MockMvc mvc @@ -31,14 +30,14 @@ class PersonControllerIntTest extends Specification { * SpringSpy will wrap the Spring injected Service with a Spy * UnwrapAopProxy will remove the cglib @Validated proxy annotated inside ExternalRankingService * However it will not use a cached test config, so many tests could be slow. - * For speed, you could just Autowire here and manually unwrap the proxy with: - * AopTestUtils.getUltimateTargetObject(externalRankingService) + * see PersonControllerIntCachedTest for how to use the spring cached context config */ @SpringSpy @UnwrapAopProxy ExternalRankingService externalRankingService def "GetRank"() { + when: 'Calling getRank for a known seed data entity' MvcResult mvcResult = mvc.perform(get("/persons/1/rank").contentType(APPLICATION_JSON)) .andExpect(status().is2xxSuccessful()).andReturn() From 64256013a7325b6bfe3a08167e278054da26209b Mon Sep 17 00:00:00 2001 From: Jeff Sheets Date: Wed, 30 May 2018 21:26:51 -0500 Subject: [PATCH 3/8] feat: updates for Spock 1.2 @SpringBean, @SpringSpy, and @UnwrapAopProxy annotations --- README.md | 2 +- .../controllers/PersonController.groovy | 9 +++++- .../objectpartners/eskens/model/Rank.groovy | 6 ++++ .../services/ExternalRankingService.groovy | 7 +--- .../eskens/services/PersonService.groovy | 10 +++++- .../ValidatedExternalRankingService.groovy | 20 ++++++++++++ .../IntegrationTestMockingConfig.groovy | 6 ++++ .../PersonControllerIntCachedTest.groovy | 32 ++++++++++++++++--- .../PersonControllerIntTest.groovy | 31 ++++++++++++++++-- .../eskens/services/PersonServiceTest.groovy | 1 + 10 files changed, 107 insertions(+), 17 deletions(-) create mode 100644 src/main/groovy/com/objectpartners/eskens/model/Rank.groovy create mode 100644 src/main/groovy/com/objectpartners/eskens/services/ValidatedExternalRankingService.groovy diff --git a/README.md b/README.md index 8fbab56..0054c69 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Thankfully Spock 1.1 introduced the `DetachedMockFactory`. This, combined with t The heart of this example lives in our `PersonControllerIntTest`. The `PersonControllerIntTest` spins up a Spring context so we can make a `MockMvc` call to a REST endpoint which pulls data from an h2 database via a Spring Data repo, but the "Rank" data we would normally get from an external service has been mocked. Updates: -* Spock 1.2 provides new @SpringSpy and @SpringBean annotations to make injecting mocks even easier. +* Spock 1.2 provides new @SpringBean, @SpringSpy, and @UnwrapAopProxy annotations to make injecting mocks even easier. * Mocking Spring proxied objects, like @Validated or @Repository, requires unwrapping the proxy to use the mock objects Testing provided by Travis CI diff --git a/src/main/groovy/com/objectpartners/eskens/controllers/PersonController.groovy b/src/main/groovy/com/objectpartners/eskens/controllers/PersonController.groovy index de0b443..03bbca4 100644 --- a/src/main/groovy/com/objectpartners/eskens/controllers/PersonController.groovy +++ b/src/main/groovy/com/objectpartners/eskens/controllers/PersonController.groovy @@ -1,7 +1,7 @@ package com.objectpartners.eskens.controllers +import com.objectpartners.eskens.model.Rank import com.objectpartners.eskens.services.PersonService -import com.objectpartners.eskens.services.Rank import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RequestMapping @@ -24,4 +24,11 @@ class PersonController { Rank rank = personService.getRank(id) return "$name ~ $rank.classification:Level $rank.level" } + + @GetMapping(path = '{id}/validatedRank') + String getNameAndValidatedRank(@PathVariable(name = 'id') Long id ) { + def name = personService.getAddressToForPersonId(id) + Rank rank = personService.getValidatedRank(id) + return "$name ~ $rank.classification:Level $rank.level" + } } diff --git a/src/main/groovy/com/objectpartners/eskens/model/Rank.groovy b/src/main/groovy/com/objectpartners/eskens/model/Rank.groovy new file mode 100644 index 0000000..55f4d7a --- /dev/null +++ b/src/main/groovy/com/objectpartners/eskens/model/Rank.groovy @@ -0,0 +1,6 @@ +package com.objectpartners.eskens.model + +class Rank { + int level + String classification +} diff --git a/src/main/groovy/com/objectpartners/eskens/services/ExternalRankingService.groovy b/src/main/groovy/com/objectpartners/eskens/services/ExternalRankingService.groovy index 55b8822..dc448ce 100644 --- a/src/main/groovy/com/objectpartners/eskens/services/ExternalRankingService.groovy +++ b/src/main/groovy/com/objectpartners/eskens/services/ExternalRankingService.groovy @@ -1,6 +1,7 @@ package com.objectpartners.eskens.services import com.objectpartners.eskens.entities.Person +import com.objectpartners.eskens.model.Rank import org.springframework.stereotype.Service import org.springframework.validation.annotation.Validated @@ -8,7 +9,6 @@ import org.springframework.validation.annotation.Validated * This is to mimic calls to an external 3rd party service that you wouldn't want to test locally. */ @Service -@Validated //This makes it a spring proxied service, so unwrapping is necessary to use a Mock class ExternalRankingService { @SuppressWarnings("GrMethodMayBeStatic") @@ -16,8 +16,3 @@ class ExternalRankingService { throw new RuntimeException('This feature is not yet implemented.') } } - -class Rank { - int level - String classification -} diff --git a/src/main/groovy/com/objectpartners/eskens/services/PersonService.groovy b/src/main/groovy/com/objectpartners/eskens/services/PersonService.groovy index b61edc9..6518fd6 100644 --- a/src/main/groovy/com/objectpartners/eskens/services/PersonService.groovy +++ b/src/main/groovy/com/objectpartners/eskens/services/PersonService.groovy @@ -1,6 +1,7 @@ package com.objectpartners.eskens.services import com.objectpartners.eskens.entities.Person +import com.objectpartners.eskens.model.Rank import com.objectpartners.eskens.repos.PersonRepo import org.springframework.stereotype.Service @@ -11,9 +12,12 @@ class PersonService { final ExternalRankingService externalRankingService - PersonService(PersonRepo pr, ExternalRankingService ers) { + final ValidatedExternalRankingService validatedExternalRankingService + + PersonService(PersonRepo pr, ExternalRankingService ers, ValidatedExternalRankingService vers) { this.personRepo = pr this.externalRankingService = ers + this.validatedExternalRankingService = vers } String getAddressToForPersonId(Long personId) { @@ -25,6 +29,10 @@ class PersonService { externalRankingService.getRank(getPerson(personId)) } + Rank getValidatedRank(Long personId) { + validatedExternalRankingService.getRank(getPerson(personId)) + } + private Person getPerson(Long personId) { personRepo.findOne(personId) } diff --git a/src/main/groovy/com/objectpartners/eskens/services/ValidatedExternalRankingService.groovy b/src/main/groovy/com/objectpartners/eskens/services/ValidatedExternalRankingService.groovy new file mode 100644 index 0000000..1ee036c --- /dev/null +++ b/src/main/groovy/com/objectpartners/eskens/services/ValidatedExternalRankingService.groovy @@ -0,0 +1,20 @@ +package com.objectpartners.eskens.services + +import com.objectpartners.eskens.entities.Person +import com.objectpartners.eskens.model.Rank +import org.springframework.stereotype.Service +import org.springframework.validation.annotation.Validated + +/** + * This class is @Validated, which makes it a spring proxied service + * so unwrapping is necessary to use a Mock + */ +@Service +@Validated +class ValidatedExternalRankingService { + + @SuppressWarnings("GrMethodMayBeStatic") + Rank getRank(Person person) { + throw new RuntimeException('This feature is not yet implemented.') + } +} diff --git a/src/test/groovy/com/objectpartners/eskens/config/IntegrationTestMockingConfig.groovy b/src/test/groovy/com/objectpartners/eskens/config/IntegrationTestMockingConfig.groovy index 7ba31ef..7eb346a 100644 --- a/src/test/groovy/com/objectpartners/eskens/config/IntegrationTestMockingConfig.groovy +++ b/src/test/groovy/com/objectpartners/eskens/config/IntegrationTestMockingConfig.groovy @@ -1,6 +1,7 @@ package com.objectpartners.eskens.config import com.objectpartners.eskens.services.ExternalRankingService +import com.objectpartners.eskens.services.ValidatedExternalRankingService import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean import spock.mock.DetachedMockFactory @@ -18,4 +19,9 @@ class IntegrationTestMockingConfig { ExternalRankingService externalRankingService() { factory.Mock(ExternalRankingService) } + + @Bean + ValidatedExternalRankingService validatedExternalRankingService() { + factory.Mock(ValidatedExternalRankingService) + } } diff --git a/src/test/groovy/com/objectpartners/eskens/controllers/PersonControllerIntCachedTest.groovy b/src/test/groovy/com/objectpartners/eskens/controllers/PersonControllerIntCachedTest.groovy index 0ec6a80..3fbdc3a 100644 --- a/src/test/groovy/com/objectpartners/eskens/controllers/PersonControllerIntCachedTest.groovy +++ b/src/test/groovy/com/objectpartners/eskens/controllers/PersonControllerIntCachedTest.groovy @@ -2,7 +2,8 @@ package com.objectpartners.eskens.controllers import com.objectpartners.eskens.config.IntegrationTestMockingConfig import com.objectpartners.eskens.services.ExternalRankingService -import com.objectpartners.eskens.services.Rank +import com.objectpartners.eskens.model.Rank +import com.objectpartners.eskens.services.ValidatedExternalRankingService import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.context.SpringBootTest @@ -28,17 +29,20 @@ class PersonControllerIntCachedTest extends Specification { @Autowired MockMvc mvc @Autowired - ExternalRankingService proxiedExternalRankingService - ExternalRankingService externalRankingService + @Autowired + ValidatedExternalRankingService proxiedValidatedExternalRankingService + + ValidatedExternalRankingService validatedExternalRankingService + void setup() { /** - * the Validated proxied ExternalRankingService must be unwrapped to use the Mock + * the Validated proxied ValidatedExternalRankingService must be unwrapped to use the Mock * * see PersonControllerIntTest to see how to make this easier with @SpringSpy @UnwrapAopProxy */ - externalRankingService = AopTestUtils.getUltimateTargetObject(proxiedExternalRankingService) + validatedExternalRankingService = AopTestUtils.getUltimateTargetObject(proxiedValidatedExternalRankingService) } def "GetRank"() { @@ -59,4 +63,22 @@ class PersonControllerIntCachedTest extends Specification { then: 'the result contains a mix of mocked service data and actual wired component data' resultingJson == 'Capt James Kirk ~ Captain:Level 1' } + + def "GetValidatedRank"() { + when: 'Calling getRank for a known seed data entity' + MvcResult mvcResult = mvc.perform(get("/persons/1/validatedRank").contentType(APPLICATION_JSON)) + .andExpect(status().is2xxSuccessful()).andReturn() + + then: 'we define the mock for the external service' + 1 * validatedExternalRankingService.getRank(_) >> { + new Rank(level: 1, classification: 'Captain') + } + noExceptionThrown() + + when: 'inspecting the contents' + def resultingJson = mvcResult.response.contentAsString + + then: 'the result contains a mix of mocked service data and actual wired component data' + resultingJson == 'Capt James Kirk ~ Captain:Level 1' + } } diff --git a/src/test/groovy/com/objectpartners/eskens/controllers/PersonControllerIntTest.groovy b/src/test/groovy/com/objectpartners/eskens/controllers/PersonControllerIntTest.groovy index b2c324e..b6ee6f0 100644 --- a/src/test/groovy/com/objectpartners/eskens/controllers/PersonControllerIntTest.groovy +++ b/src/test/groovy/com/objectpartners/eskens/controllers/PersonControllerIntTest.groovy @@ -1,7 +1,9 @@ package com.objectpartners.eskens.controllers +import com.objectpartners.eskens.model.Rank import com.objectpartners.eskens.services.ExternalRankingService -import com.objectpartners.eskens.services.Rank +import com.objectpartners.eskens.services.ValidatedExternalRankingService +import org.spockframework.spring.SpringBean import org.spockframework.spring.SpringSpy import org.spockframework.spring.UnwrapAopProxy import org.springframework.beans.factory.annotation.Autowired @@ -34,10 +36,15 @@ class PersonControllerIntTest extends Specification { */ @SpringSpy @UnwrapAopProxy - ExternalRankingService externalRankingService + ValidatedExternalRankingService validatedExternalRankingService - def "GetRank"() { + /** + * SpringBean will put the mock into the spring context + */ + @SpringBean + ExternalRankingService externalRankingService = Mock() + def "GetRank"() { when: 'Calling getRank for a known seed data entity' MvcResult mvcResult = mvc.perform(get("/persons/1/rank").contentType(APPLICATION_JSON)) .andExpect(status().is2xxSuccessful()).andReturn() @@ -54,4 +61,22 @@ class PersonControllerIntTest extends Specification { then: 'the result contains a mix of mocked service data and actual wired component data' resultingJson == 'Capt James Kirk ~ Captain:Level 1' } + + def "GetValidatedRank"() { + when: 'Calling getRank for a known seed data entity' + MvcResult mvcResult = mvc.perform(get("/persons/1/validatedRank").contentType(APPLICATION_JSON)) + .andExpect(status().is2xxSuccessful()).andReturn() + + then: 'we define the mock for the external service' + 1 * validatedExternalRankingService.getRank(_) >> { + new Rank(level: 1, classification: 'Captain') + } + noExceptionThrown() + + when: 'inspecting the contents' + def resultingJson = mvcResult.response.contentAsString + + then: 'the result contains a mix of mocked service data and actual wired component data' + resultingJson == 'Capt James Kirk ~ Captain:Level 1' + } } diff --git a/src/test/groovy/com/objectpartners/eskens/services/PersonServiceTest.groovy b/src/test/groovy/com/objectpartners/eskens/services/PersonServiceTest.groovy index c2645bb..34c3d41 100644 --- a/src/test/groovy/com/objectpartners/eskens/services/PersonServiceTest.groovy +++ b/src/test/groovy/com/objectpartners/eskens/services/PersonServiceTest.groovy @@ -3,6 +3,7 @@ package com.objectpartners.eskens.services import com.blogspot.toomuchcoding.spock.subjcollabs.Collaborator import com.blogspot.toomuchcoding.spock.subjcollabs.Subject import com.objectpartners.eskens.entities.Person +import com.objectpartners.eskens.model.Rank import com.objectpartners.eskens.repos.PersonRepo import spock.lang.Specification From 3591b10feda7678a98290296043d7922cef65678 Mon Sep 17 00:00:00 2001 From: Jeff Sheets Date: Wed, 30 May 2018 21:34:41 -0500 Subject: [PATCH 4/8] chore: cleanup to match original blog post --- ... => PersonControllerIntSpock12Test.groovy} | 45 +++++++------ .../PersonControllerIntTest.groovy | 65 ++++++++++++++----- 2 files changed, 71 insertions(+), 39 deletions(-) rename src/test/groovy/com/objectpartners/eskens/controllers/{PersonControllerIntCachedTest.groovy => PersonControllerIntSpock12Test.groovy} (73%) diff --git a/src/test/groovy/com/objectpartners/eskens/controllers/PersonControllerIntCachedTest.groovy b/src/test/groovy/com/objectpartners/eskens/controllers/PersonControllerIntSpock12Test.groovy similarity index 73% rename from src/test/groovy/com/objectpartners/eskens/controllers/PersonControllerIntCachedTest.groovy rename to src/test/groovy/com/objectpartners/eskens/controllers/PersonControllerIntSpock12Test.groovy index 3fbdc3a..03a2eb5 100644 --- a/src/test/groovy/com/objectpartners/eskens/controllers/PersonControllerIntCachedTest.groovy +++ b/src/test/groovy/com/objectpartners/eskens/controllers/PersonControllerIntSpock12Test.groovy @@ -1,14 +1,14 @@ package com.objectpartners.eskens.controllers -import com.objectpartners.eskens.config.IntegrationTestMockingConfig -import com.objectpartners.eskens.services.ExternalRankingService import com.objectpartners.eskens.model.Rank +import com.objectpartners.eskens.services.ExternalRankingService import com.objectpartners.eskens.services.ValidatedExternalRankingService +import org.spockframework.spring.SpringBean +import org.spockframework.spring.SpringSpy +import org.spockframework.spring.UnwrapAopProxy import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.context.SpringBootTest -import org.springframework.context.annotation.Import -import org.springframework.test.util.AopTestUtils import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.MvcResult import spock.lang.Specification @@ -20,33 +20,32 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. /** * An integration test illustrating how to wire everything w/ Spring, * but replace certain components with Spock mocks + * + * Uses Spock 1.2 annotations of @SpringBean, @SpringSpy, and @UnwrapAopProxy */ @SpringBootTest @AutoConfigureMockMvc -@Import([IntegrationTestMockingConfig]) //uses a cached spring context for speed -class PersonControllerIntCachedTest extends Specification { +class PersonControllerIntSpock12Test extends Specification { @Autowired MockMvc mvc - @Autowired - ExternalRankingService externalRankingService - - @Autowired - ValidatedExternalRankingService proxiedValidatedExternalRankingService - + /** + * SpringBean will put the mock into the spring context + */ + @SpringBean + ExternalRankingService externalRankingService = Mock() + + /** + * SpringSpy will wrap the Spring injected Service with a Spy + * UnwrapAopProxy will remove the cglib @Validated proxy annotated inside ExternalRankingService + * However it will not use a cached test config, so many tests could be slow. + * see PersonControllerIntTest for how to use the spring cached context config + */ + @SpringSpy + @UnwrapAopProxy ValidatedExternalRankingService validatedExternalRankingService - void setup() { - /** - * the Validated proxied ValidatedExternalRankingService must be unwrapped to use the Mock - * - * see PersonControllerIntTest to see how to make this easier with @SpringSpy @UnwrapAopProxy - */ - validatedExternalRankingService = AopTestUtils.getUltimateTargetObject(proxiedValidatedExternalRankingService) - } - def "GetRank"() { - when: 'Calling getRank for a known seed data entity' MvcResult mvcResult = mvc.perform(get("/persons/1/rank").contentType(APPLICATION_JSON)) .andExpect(status().is2xxSuccessful()).andReturn() @@ -67,7 +66,7 @@ class PersonControllerIntCachedTest extends Specification { def "GetValidatedRank"() { when: 'Calling getRank for a known seed data entity' MvcResult mvcResult = mvc.perform(get("/persons/1/validatedRank").contentType(APPLICATION_JSON)) - .andExpect(status().is2xxSuccessful()).andReturn() + .andExpect(status().is2xxSuccessful()).andReturn() then: 'we define the mock for the external service' 1 * validatedExternalRankingService.getRank(_) >> { diff --git a/src/test/groovy/com/objectpartners/eskens/controllers/PersonControllerIntTest.groovy b/src/test/groovy/com/objectpartners/eskens/controllers/PersonControllerIntTest.groovy index b6ee6f0..0b35a3d 100644 --- a/src/test/groovy/com/objectpartners/eskens/controllers/PersonControllerIntTest.groovy +++ b/src/test/groovy/com/objectpartners/eskens/controllers/PersonControllerIntTest.groovy @@ -1,14 +1,14 @@ package com.objectpartners.eskens.controllers +import com.objectpartners.eskens.config.IntegrationTestMockingConfig import com.objectpartners.eskens.model.Rank import com.objectpartners.eskens.services.ExternalRankingService import com.objectpartners.eskens.services.ValidatedExternalRankingService -import org.spockframework.spring.SpringBean -import org.spockframework.spring.SpringSpy -import org.spockframework.spring.UnwrapAopProxy import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.annotation.Import +import org.springframework.test.util.AopTestUtils import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.MvcResult import spock.lang.Specification @@ -24,25 +24,29 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. */ @SpringBootTest @AutoConfigureMockMvc +@Import([IntegrationTestMockingConfig]) //See additional notes at the bottom class PersonControllerIntTest extends Specification { @Autowired MockMvc mvc /** - * SpringSpy will wrap the Spring injected Service with a Spy - * UnwrapAopProxy will remove the cglib @Validated proxy annotated inside ExternalRankingService - * However it will not use a cached test config, so many tests could be slow. - * see PersonControllerIntCachedTest for how to use the spring cached context config + * This is our mock we created in our test config. We inject it in so we can control it in our specs. */ - @SpringSpy - @UnwrapAopProxy + @Autowired ExternalRankingService externalRankingServiceMock + + @Autowired + ValidatedExternalRankingService proxiedValidatedExternalRankingService + ValidatedExternalRankingService validatedExternalRankingService - /** - * SpringBean will put the mock into the spring context - */ - @SpringBean - ExternalRankingService externalRankingService = Mock() + void setup() { + /** + * the Validated proxied ValidatedExternalRankingService must be unwrapped to use the Mock + * + * see PersonControllerIntTest to see how to make this easier with @SpringSpy @UnwrapAopProxy + */ + validatedExternalRankingService = AopTestUtils.getUltimateTargetObject(proxiedValidatedExternalRankingService) + } def "GetRank"() { when: 'Calling getRank for a known seed data entity' @@ -50,7 +54,7 @@ class PersonControllerIntTest extends Specification { .andExpect(status().is2xxSuccessful()).andReturn() then: 'we define the mock for JUST the external service' - 1 * externalRankingService.getRank(_) >> { + externalRankingServiceMock.getRank(_) >> { new Rank(level: 1, classification: 'Captain') } noExceptionThrown() @@ -65,7 +69,7 @@ class PersonControllerIntTest extends Specification { def "GetValidatedRank"() { when: 'Calling getRank for a known seed data entity' MvcResult mvcResult = mvc.perform(get("/persons/1/validatedRank").contentType(APPLICATION_JSON)) - .andExpect(status().is2xxSuccessful()).andReturn() + .andExpect(status().is2xxSuccessful()).andReturn() then: 'we define the mock for the external service' 1 * validatedExternalRankingService.getRank(_) >> { @@ -79,4 +83,33 @@ class PersonControllerIntTest extends Specification { then: 'the result contains a mix of mocked service data and actual wired component data' resultingJson == 'Capt James Kirk ~ Captain:Level 1' } + + + /* + We could define our test configuration here, but if we have multiple integration tests + and we want to mock the same things, then it's better to share the configuration for context caching, + thus the import of IntegrationTestMockingConfig + + + If you are using Spock 1.2, see PersonControllerInSpock12Test for usage of @SpringBean annotation + + Otherwise use the below code for Spock <= 1.1 + */ + + /* + @TestConfiguration + static class Config { + private DetachedMockFactory factory = new DetachedMockFactory() + + @Bean + ExternalRankingService externalRankingService() { + factory.Mock(ExternalRankingService) + } + + @Bean + ValidatedExternalRankingService validatedExternalRankingService() { + factory.Mock(ValidatedExternalRankingService) + } + } + */ } From f223955f67645886fc216fbafb2628c7df045027 Mon Sep 17 00:00:00 2001 From: Jeff Sheets Date: Wed, 30 May 2018 21:37:40 -0500 Subject: [PATCH 5/8] chore: small cleanup to match original --- .../eskens/services/ExternalRankingService.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/groovy/com/objectpartners/eskens/services/ExternalRankingService.groovy b/src/main/groovy/com/objectpartners/eskens/services/ExternalRankingService.groovy index dc448ce..1db619b 100644 --- a/src/main/groovy/com/objectpartners/eskens/services/ExternalRankingService.groovy +++ b/src/main/groovy/com/objectpartners/eskens/services/ExternalRankingService.groovy @@ -3,10 +3,10 @@ package com.objectpartners.eskens.services import com.objectpartners.eskens.entities.Person import com.objectpartners.eskens.model.Rank import org.springframework.stereotype.Service -import org.springframework.validation.annotation.Validated /** * This is to mimic calls to an external 3rd party service that you wouldn't want to test locally. + * Created by derek on 4/10/17. */ @Service class ExternalRankingService { From 9656eb2f00e17e89d5433296605e996b7970a1e0 Mon Sep 17 00:00:00 2001 From: Jeff Sheets Date: Wed, 30 May 2018 21:46:35 -0500 Subject: [PATCH 6/8] docs: updated readme --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0054c69..3a41f6b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +**This is a Spock 1.2 update of the original fork** + **How to inject Spock mocks into Spring integration tests** This project is intended to be used as en example guide to illustrate how you can use Spock with Spring (and Spring Boot) with a mix of Spring configuration and Spock mocks. @@ -11,8 +13,5 @@ Thankfully Spock 1.1 introduced the `DetachedMockFactory`. This, combined with t The heart of this example lives in our `PersonControllerIntTest`. The `PersonControllerIntTest` spins up a Spring context so we can make a `MockMvc` call to a REST endpoint which pulls data from an h2 database via a Spring Data repo, but the "Rank" data we would normally get from an external service has been mocked. Updates: -* Spock 1.2 provides new @SpringBean, @SpringSpy, and @UnwrapAopProxy annotations to make injecting mocks even easier. +* Spock 1.2 provides new @SpringBean, @SpringSpy, and @UnwrapAopProxy annotations to make injecting mocks even easier. See `PersonControllerIntSpock12Test` * Mocking Spring proxied objects, like @Validated or @Repository, requires unwrapping the proxy to use the mock objects - -Testing provided by Travis CI -[![Build Status](https://travis-ci.org/snekse/spring-spock-integration-testing.svg?branch=master)](https://travis-ci.org/snekse/spring-spock-integration-testing) \ No newline at end of file From ed77a2efa1aeaa20d710d26227811f7be015db10 Mon Sep 17 00:00:00 2001 From: Jeff Sheets Date: Wed, 30 May 2018 21:47:39 -0500 Subject: [PATCH 7/8] docs: updated readme --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 3a41f6b..d233852 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ **This is a Spock 1.2 update of the original fork** +Original blog post here: https://objectpartners.com/2017/04/18/spring-integration-testing-with-spock-mocks/ + +Original source code here: https://github.com/snekse/spring-spock-integration-testing + **How to inject Spock mocks into Spring integration tests** This project is intended to be used as en example guide to illustrate how you can use Spock with Spring (and Spring Boot) with a mix of Spring configuration and Spock mocks. From 3c22f74634a263caea73eaa3c0bbce071ee34839 Mon Sep 17 00:00:00 2001 From: Jeff Sheets Date: Wed, 30 May 2018 21:49:55 -0500 Subject: [PATCH 8/8] docs: improve attribution --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d233852..2b2a697 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -**This is a Spock 1.2 update of the original fork** +**This is a Spock 1.2 update of the original code from Derek Eskens @snekse** Original blog post here: https://objectpartners.com/2017/04/18/spring-integration-testing-with-spock-mocks/