groovy 如何在Spock中指定动态调用目标Assert

ibrsph3r  于 7个月前  发布在  其他
关注(0)|答案(2)|浏览(82)

考虑以下示例

@AllArgsConstructor
class Foo{
  private AHandler ahandler;
  private BHandler bhandler;

  public void handle(String what){

     if(what.equals("A")){
          ahandler.handle(what);
     }else if(what.equals("B"){
       bhandler.handle(what)
     }else throw new RuntimeException();
  }
}

现在我尝试测试if逻辑,其中必须调用给定的输入适当的处理程序,但我不知道如何动态指定Assert。以下不起作用(但我认为显示了意图)

def ahandler=Mock(AHandler);
def bhandler=Mock(BHandler);
def service=new Foo(ahandler,bhandler);

when:
   service.handle(input);
then: 
   1 * expectedCalls
   0 * _ //no other calls
where:
input | expectedCalls
"A" | ahandler.handle("A")
"B" | bhandler.handle("B")

有没有可能和斯波克做这种事?

pbpqsu0x

pbpqsu0x1#

像往常一样,经过一些尝试和错误(谷歌和检查github spock问题),我找到了令人满意的解决方案。

then: 
   1 * this."$handler".handle(_)
   0 * _ //no other calls
where:
input | handler
"A" | "ahandler"
"B" | "bhandler"
8ljdwjyq

8ljdwjyq2#

虽然您自己的解决方案在修复了错误和不完整的示例代码后仍然有效,但我认为这种过度设计的解决方案并不好。

Antonio解决方案

首先,您的原始代码假设您有方法AHandler.handle(String),但BHandler.handler(String)在方法名称的末尾带有“r”。斯波克规格假设相同。然后,在你自己的答案中,你切换到了两个处理程序类的统一handle(String)方法,这可能更有意义,但考虑到你的问题,这样解决是不可能的。
this."$handler".handle(_)中需要显式的前导this.,这也是非常丑陋的。一旦您将mock定义从feature方法中的字段移动到局部变量,这种解决方案也会崩溃。因此,解决方案是脆弱的。无论如何,下面是一个MCVE,它再现了您的问题和解决方案:

class AHandler {
  void handle(String s) {}
}
class BHandler {
  void handle(String s) {}
}
class Foo {
  private AHandler aHandler
  private BHandler bHandler

  Foo(AHandler aHandler, BHandler bHandler) {
    this.aHandler = aHandler
    this.bHandler = bHandler
  }

  void handle(String what) {
    if (what.equals("A"))
      aHandler.handle(what)
    else if (what.equals("B"))
      bHandler.handle(what)
    else
      throw new RuntimeException()
  }
}
import spock.lang.Specification

class DynamicInvocationTargetTest extends Specification {
  def aHandler = Mock(AHandler)
  def bHandler = Mock(BHandler)
  def service = new Foo(aHandler, bHandler)

  def test() {
    when:
    service.handle(input)

    then:
    1 * this."$handler".handle(input)
    0 * _  // no other calls

    where:
    input | handler
    'A'   | 'aHandler'
    'B'   | 'bHandler'
  }
}

更优雅的解决方案

相反,你可以这样做,甚至使用局部变量进行模拟:

import spock.lang.Specification

class DynamicInvocationTargetTest extends Specification {
  def test() {
    given:
    def handlers = [ A: Mock(AHandler), B: Mock(BHandler)]
    def service = new Foo(handlers['A'], handlers['B'])

    when:
    service.handle(input)

    then:
    1 * handlers[input].handle(input)
    0 * _  // no other calls

    where:
    input << ['A', 'B']
  }
}

或者,假设两个处理程序类都实现了一个公共接口,我们可以设计一个更类型安全的解决方案:

interface MyHandler {
  void handle(String s)
}
class AHandler implements MyHandler {
  @Override
  void handle(String s) {}
}
class BHandler {
  @Override
  void handle(String s) {}
}

现在,我的IDE自动将def handlers解释为Map<String, MyHandler> handlers,并且.handle(input)调用的未知调用目标的警告消失了。
当然,如果Foo中的字段定义的类型是MyHandler而不是AHandlerBHandler,那么可以从Spock规范中删除更多的警告,但这超出了范围,也许你需要确切的类型。我知道你的代码只是示例代码。我只是想指出解决这个问题的方法,而不需要解析字符串来解析方法名。
Groovy Web Console中尝试。

我的解决方案的外观变化

**更新:**我重新访问了这段代码,因为我正在处理另一个问题,想知道我们是否可以不使用map,也不需要以DRY的方式在所有地方重复'A''B'。这是其中之一:

class DynamicInvocationTargetTest extends Specification {
  def test() {
    given:
    def handlers = [Mock(AHandler), Mock(BHandler)]
    def service = new Foo(*handlers)

    when:
    service.handle(input)

    then:
    1 * handlers[index].handle(input)
    0 * _  // no other calls

    where:
    index | input
    0     | 'A'
    1     | 'B'
  }
}

有些更深奥,避免index数据变量,并利用Spock 2.0 iterationIndex。我并不推荐这个变体,只是为了完整起见而提到它:

class DynamicInvocationTargetTest extends Specification {
  def test() {
    given:
    def handlers = [Mock(AHandler), Mock(BHandler)]
    def service = new Foo(*handlers)

    when:
    service.handle(input)

    then:
    1 * handlers[specificationContext.currentIteration.iterationIndex].handle(input)
    0 * _  // no other calls

    where:
    input << ['A', 'B']
  }
}

请注意,这两种解决方案都使用spread操作符来调用构造函数new Foo(*handlers)。现在我们有了一个有序的mock列表而不是一个无序的map,使用spread操作符是安全的。

相关问题