【问题标题】:Capture spock return value and use it for verification捕获 spock 返回值并用于验证
【发布时间】:2020-07-04 21:08:33
【问题描述】:

如何测试是否使用特定动态值调用了模拟 logService 方法 - 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
  }
}

也许我可以捕获 PersonEntity?

我不想注入 UUID 提供程序只是为了生成 UUID 并在测试中模拟它。但我可以模拟/存根 personRepository(它是由 spring 注入的)。

【问题讨论】:

    标签: mocking spock spring-test


    【解决方案1】:

    您不能在交互中使用createdPersonId,因为then: 块中的模拟交互实际上已转换为在when: 块之前定义。你有一个引导或母鸡与鸡蛋的问题,请参阅my other answer。在定义该方法调用中使用的模拟的所需行为和交互时,您不能使用被测方法调用的结果。

    不过,你可以做这样的事情(对不起,我不得不推测你的问题中没有显示的依赖类):

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

    更新:以下是重构应用程序以使其更具可测试性的两种变体。

    变体 1:介绍创建 ID 的方法

    在这里,我们将 ID 创建分解到一个受保护的方法 createId() 中,然后我们可以在称为 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()
        }
      }
    
    }
    

    变体 2:为创建 ID 引入类

    在这里,我们将 ID 创建分解为一个易于模拟的专用类 IdCreator。它将 ID 创建与PersonService 分离。不需要使用像Spy(PersonService) 这样的花哨的东西,一个带有注入 ID 创建者的普通服务实例就足够了。即使在生产使用中,也很容易将 UUID 创建器重新用于其他对象 ID,单独测试它,通过子类覆盖它,甚至重构为具有不同实现的接口以用于不同目的。你可能认为这是过度设计,但我认为不是。解耦和可测试性是软件设计中必不可少的东西。

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

    【讨论】:

    • 感谢您的回答。抱歉,我没有提供依赖项,我已经用 PersonService 代码重新填写了我的问题,请再次查看。 id 是动态生成的:def id = UUID.randomUUID().toString()。而且我不想注入 uuuidProvider 只是为了在测试中模拟它。但是在PersonService中我有PersonRepository personRepository,可以注入。也许可以捕获 personRepository 持续存在的内容?可能我用 mockito 误导了 spock,AFAIR 我需要并在 mockito 中使用了这种捕获。
    • 好吧,问题还是一样。您想捕获savePerson 的返回值并验证logSavedId 是使用该返回值调用的,但调用是从savePerson 内部进行的在方法返回之前。除非您有时间机器,否则您以后不能使用返回值来检查之前是否发生过某些事情。这不是 Spock 的限制,只是不合逻辑。
    • 问题的根本原因是几乎无法测试的代码,因为您想访问像 idpersonEntity 这样的依赖项,这些依赖项不是注入而是在方法中作为局部变量创建的。
    • 更新: 我添加了两种重构变体,我建议您使用它们。两者都有其优点和缺点,但它们的共同点是它们使您的代码更具可测试性。顺便说一句,我更喜欢第二种变体。
    猜你喜欢
    • 2018-08-06
    • 2017-04-30
    • 2016-05-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-04-01
    • 1970-01-01
    • 2013-01-21
    相关资源
    最近更新 更多