bastiat
bastiat

Reputation: 2071

Capture spock return value and use it for verification

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

Answers (1)

kriegaex
kriegaex

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.

Variant 1: introduce method for ID creation

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()
    }
  }

}

Variant 2: introduce class for ID creation

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

Related Questions