Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.gradle
/build/
!gradle/wrapper/gradle-wrapper.jar
out

### NPM ###
/npm-debug.log
Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
**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/

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.
Expand All @@ -10,5 +16,6 @@ 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.

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)
Updates:
* 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
11 changes: 8 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
buildscript {
ext {
springBootVersion = '1.5.2.RELEASE'
springBootVersion = '1.5.13.RELEASE'
}
repositories {
mavenCentral()
Expand All @@ -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/'
}
}


Expand All @@ -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")
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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"
}
}
6 changes: 6 additions & 0 deletions src/main/groovy/com/objectpartners/eskens/model/Rank.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.objectpartners.eskens.model

class Rank {
int level
String classification
}
Original file line number Diff line number Diff line change
@@ -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

/**
Expand All @@ -15,8 +16,3 @@ class ExternalRankingService {
throw new RuntimeException('This feature is not yet implemented.')
}
}

class Rank {
int level
String classification
}
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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) {
Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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.')
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -18,4 +19,9 @@ class IntegrationTestMockingConfig {
ExternalRankingService externalRankingService() {
factory.Mock(ExternalRankingService)
}

@Bean
ValidatedExternalRankingService validatedExternalRankingService() {
factory.Mock(ValidatedExternalRankingService)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.objectpartners.eskens.controllers

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.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
*
* Uses Spock 1.2 annotations of @SpringBean, @SpringSpy, and @UnwrapAopProxy
*/
@SpringBootTest
@AutoConfigureMockMvc
class PersonControllerIntSpock12Test extends Specification {

@Autowired MockMvc mvc

/**
* 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

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'
}

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'
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package com.objectpartners.eskens.controllers

import com.objectpartners.eskens.config.IntegrationTestMockingConfig
import com.objectpartners.eskens.entities.Person
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.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
Expand All @@ -33,6 +34,20 @@ class PersonControllerIntTest extends Specification {
*/
@Autowired ExternalRankingService externalRankingServiceMock

@Autowired
ValidatedExternalRankingService proxiedValidatedExternalRankingService

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))
Expand All @@ -51,11 +66,34 @@ class PersonControllerIntTest extends Specification {
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'
}


/*
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
*/

/*
Expand All @@ -67,6 +105,11 @@ class PersonControllerIntTest extends Specification {
ExternalRankingService externalRankingService() {
factory.Mock(ExternalRankingService)
}

@Bean
ValidatedExternalRankingService validatedExternalRankingService() {
factory.Mock(ValidatedExternalRankingService)
}
}
*/
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down