Reputation: 2071
How can I test if a mocked logService method was called with specific dynamic value - createdPersonId ?
@SpringBean
private LogService logService = Mock(LogService)
def "test if id is logged"() {
when:
createdPersonId = personService.savePerson(personRequest)
then:
1 * logService.logSavedId(_) // it works fine
1 * logService.logSavedId(createdPersonId) // it doesn't work, createdPersonId is expected to be null instead of real value
}
static class PersonService {
LogService logService
PersonRepository personRepository
int savePerson(PersonRequest personRequest) {
def id = UUID.randomUUID().toString()
PersonEntity personEntity = mapRequestToEntity(personRequest)
entity.id = id
personRepository.persist(personEntity)
logService.logSavedId(id)
return id
}
}
Possibly I could capture PersonEntity?
I don't want to inject UUID provider just to generate UUID and mock it in test. But I can mock/stub personRepository (it's injected by spring).
Upvotes: 1
Views: 600
Reputation: 67317
You cannot use createdPersonId
in the interaction because the mock interaction in the then:
block is actually transformed to being defined before the when:
block. You have a bootstrapping or hen vs. egg problem there, see my other answer. You cannot use the result of the method call under test when defining the desired behaviour and interactions of the mock used in that method call.
You could do something like this, though (sorry, I have to speculate about the dependency classes you don't show in your question):
package de.scrum_master.stackoverflow
import spock.lang.Specification
class PersonServiceTest extends Specification {
private LogService logService = Mock(LogService)
def "test if id is logged"() {
given:
def person = new Person(id: 11, name: "John Doe")
def personRequest = new PersonRequest(person: person)
def personService = new PersonService(logService: logService)
def id = personRequest.person.id
when:
def createdPersonId = personService.savePerson(personRequest)
then:
1 * logService.logSavedId(id)
createdPersonId == id
}
static class Person {
int id
String name
}
static class PersonRequest {
Person person
}
static class LogService {
void logSavedId(int id) {
println "Logged ID = $id"
}
}
static class PersonService {
LogService logService
int savePerson(PersonRequest personRequest) {
def id = personRequest.person.id
logService.logSavedId(id)
return id
}
}
}
Update: Here are two variants of how you can refactor your application in order to make it more testable.
Here we factor out ID creation into a protected method createId()
which then we can stub out in a partial mock called a Spy
:
package de.scrum_master.stackoverflow.q60829903
import spock.lang.Specification
class PersonServiceTest extends Specification {
def logService = Mock(LogService)
def "test if id is logged"() {
given:
def person = new Person(name: "John Doe")
def personRequest = new PersonRequest(person: person)
and:
def personId = "012345-6789-abcdef"
def personService = Spy(PersonService) {
createId() >> personId
}
personService.logService = logService
when:
personService.savePerson(personRequest)
then:
1 * logService.logSavedId(personId)
person.id == personId
}
static class Person {
String id
String name
}
static class PersonRequest {
Person person
}
static class LogService {
void logSavedId(String id) {
println "Logged ID = $id"
}
}
static class PersonService {
LogService logService
String savePerson(PersonRequest personRequest) {
def id = createId()
personRequest.person.id = id
logService.logSavedId(id)
return id
}
protected String createId() {
return UUID.randomUUID().toString()
}
}
}
Here we factor out ID creation into a dedicated class IdCreator
which is easy to mock. It decouples ID creation from the PersonService
. There is no need to use fancy things like Spy(PersonService)
, a normal service instance with an injected ID creator is enough. Even in production use it would be easy to re-use the UUID creator for other object IDs, to test it separately, to override it by a subclass or to even refactor into an interface with different implementations for different purposes. You might think this is over-engineering, but I think it is not. Decoupling and testability are essential things to have in software design.
package de.scrum_master.stackoverflow.q60829903
import spock.lang.Specification
class PersonServiceTest extends Specification {
def logService = Mock(LogService)
def "test if id is logged"() {
given:
def person = new Person(name: "John Doe")
def personRequest = new PersonRequest(person: person)
and:
def personId = "012345-6789-abcdef"
def idCreator = Stub(IdCreator) {
createId() >> personId
}
def personService = new PersonService(logService, idCreator)
when:
personService.savePerson(personRequest)
then:
1 * logService.logSavedId(personId)
person.id == personId
}
static class Person {
String id
String name
}
static class PersonRequest {
Person person
}
static class LogService {
void logSavedId(String id) {
println "Logged ID = $id"
}
}
static class IdCreator {
String createId() {
return UUID.randomUUID().toString()
}
}
static class PersonService {
LogService logService
IdCreator idCreator
PersonService(LogService logService) {
this(logService, new IdCreator())
}
PersonService(LogService logService, IdCreator idCreator) {
this.logService = logService
this.idCreator = idCreator
}
String savePerson(PersonRequest personRequest) {
def id = idCreator.createId()
personRequest.person.id = id
logService.logSavedId(id)
return id
}
}
}
Upvotes: 1