【问题标题】:Spock - How to work with repeated interactionsSpock - 如何处理重复交互
【发布时间】:2019-10-20 05:32:48
【问题描述】:

对于少数测试用例,我尝试遵循 DRY 原则,其中只有交互在相同的测试用例条件下有所不同。我找不到在interaction { } 块中实现多种方法的方法。

正如http://spockframework.org/spock/docs/1.3/interaction_based_testing.html#_explicit_interaction_blocks 中所述,我在then: 块中使用interaction { },如下所示:

Java 代码:

// legacy code (still running on EJB 1.0 framework, and no dependency injection involved)
// can't alter java code base

public voidGetData() {
  DataService ds = new DataService();
  ds = ds.findByOffset(5);
  Long len = ds.getOffset() // happy path scenario; missing a null check
  // other code
}

// other varieties of same code:
public voidGetData2() {
  ItemEJB tmpItem = new ItemEJB();
  ItemEJB item = tmpItem.findByOffset(5);
  if(null != item) {
    Long len = item.getOffset();
    // other code
  }
}

public voidGetData3() {
  ItemEJB item = new ItemEJB().findByOffset(5);
  if(null != item) {
    Long len = item.getOffset();
    // other code
  }
}

Spock 测试:

def "test scene1"() {
  given: "a task"
  // other code ommitted
  DataService mockObj = Mock(DataService)

  when: "take action"
  // code omitted

  then: "action response"
  interaction {
    verifyNoDataScenario()    // How to add verifyErrorScenario() interaction to the list?
  }
}

private verifyDataScenario() {
  1 * mockObj.findByOffset(5) >> mockObj // the findByOffset() returns an object, so mapped to same mock instance
  1 * mockObj.getOffset() >> 200
}

private verifyErrorScenario() {
  1 * mockObj.findByOffset(5) >> null // the findByOffset() returns null
  0 * mockObj.getOffset() >> 200  // this won't be executed, and should ie expected to throw NPE
}

interaction 闭包不接受多个方法调用。我不确定这是否是设计限制。我相信在闭包中可以做的不仅仅是提到方法名称。我还尝试将 mockObj 插入为变量并使用数据管道/数据表,但由于它引用了相同的模拟实例,所以它不起作用。我将把它作为一个单独的问题发布。

我最终重复了两次测试用例,只是为了调用不同的交互方法。下线我看到更多的场景,并希望避免复制和粘贴方法。感谢任何实现这一目标的指针。

更新:

  • 修改了共享 java 代码,因为早期的 DataService 名称令人困惑。
  • 由于不涉及 DI,而且我没有找到模拟方法变量的方法,所以我使用 PowerMockito 模拟它们,例如PowerMockito.whenNew(DataService.class).withNoArguments().thenReturn(mockObj)

【问题讨论】:

  • 你好。你问了一个问题,我回答了。在您提出后续问题后,您没有提供我要求的 MCVE。我会很感激你接受+赞成我的回答,因为它非常详细地回答了你最初的问题。

标签: unit-testing spock powermock powermockito


【解决方案1】:

您的应用程序代码看起来很奇怪。你遗留应用程序中的编程风格真的那么糟糕吗?首先使用无参数构造函数创建 DataService 对象,只是在下一步中通过调用该实例上的方法来覆盖,该方法再次返回 DataService 对象。什么样的程序员会创建这样的代码?还是您只是编写了一些与您的实际应用程序没有太多共同点的伪代码?请解释一下。

至于您的测试代码,它也没有任何意义,因为您在功能方法(测试方法)中将DataService mockObj 实例化为局部变量,这意味着在您的辅助方法中mockObj 无法访问。因此,您要么需要将对象作为参数传递给辅助方法,要么需要将其作为测试类中的字段。

最后但同样重要的是,您的本地模拟对象永远不会注入到被测类中,因为正如我在第一段中所说,getData() 中的DataService 对象也是一个局部变量。除非您的应用程序代码完全是假的,否则无法注入模拟,因为getData() 没有任何方法参数并且DataService 对象不是可以通过setter 方法或构造函数设置的字段。因此,您可以根据需要创建任意数量的模拟,应用程序永远不会知道它们。所以你的存根findByOffset(long offset)(为什么不显示该方法的代码?)没有任何效果。

底线:请提供一个反映您的真实代码结构的示例,包括应用程序和测试代码。不幸的是,您提供的 sn-ps 没有任何意义。我想帮忙,但像这样我不能。


更新:

在我的 cmets 中,我提到通过添加构造函数、setter 方法或带有附加参数的重载 getData 方法来重构遗留代码以实现可测试性。这是我的意思的一个例子:

虚拟助手类:

package de.scrum_master.stackoverflow.q58470315;

public class DataService {
  private long offset;

  public DataService(long offset) {
    this.offset = offset;
  }

  public DataService() {}

  public DataService findByOffset(long offset) {
    return new DataService(offset);
  }

  public long getOffset() {
    return offset;
  }

  @Override
  public String toString() {
    return "DataService{" +
      "offset=" + offset +
      '}';
  }
}

被测对象:

让我添加一个带有 setter 的私有 DataService 成员,以使对象可注入。我还添加了一个检查 ds 成员是否已被注入。如果没有,代码将像以前一样在生产环境中运行,并自行创建一个新对象。

package de.scrum_master.stackoverflow.q58470315;

public class ToBeTestedWithInteractions {
  private DataService ds;

  public void setDataService(DataService ds) {
    this.ds = ds;
  }

  // legacy code; can't alter
  public void getData() {
    if (ds == null)
      ds = new DataService();
    ds = ds.findByOffset(5);
    Long len = ds.getOffset();
  }
}

Spock 测试:

现在让我们测试正常和错误情况。实际上,我认为您应该将其分解为两个较小的功能方法,但是由于您似乎希望在一种方法中测试所有内容(IMO 太多),您也可以通过两对不同的 when-then 块来做到这一点。您无需显式声明任何交互块即可。

package de.scrum_master.stackoverflow.q58470315

import spock.lang.Specification

class RepeatedInteractionsTest extends Specification {
  def "test scene1"() {
    given: "subject under test with injected mock"
    ToBeTestedWithInteractions subjectUnderTest = new ToBeTestedWithInteractions()
    DataService dataService = Mock()
    subjectUnderTest.dataService = dataService

    when: "getting data"
    subjectUnderTest.getData()

    then: "no error, normal return values"
    noExceptionThrown()
    1 * dataService.findByOffset(5) >> dataService
    1 * dataService.getOffset() >> 200

    when: "getting data"
    subjectUnderTest.getData()

    then: "NPE, only first method called"
    thrown NullPointerException
    1 * dataService.findByOffset(5) >> null
    0 * dataService.getOffset()
  }
}

另请注意,测试抛出或未抛出的异常会增加测试的价值,交互测试只是检查内部遗留代码行为,这几乎没有价值。

【讨论】:

  • 我很欣赏你的回复,但我担心java代码来自当前的生活环境。这是因为使用了旧的企业框架,而我们仍处于 EJB 1.0 的世界中,还没有 DI。你相信吗?我更新了另一个 java sn-p 以便于解释。如果java代码比较混乱,你可以忽略它,请随意举个简单的例子。主要关注点是如何为相同的给定:、何时:、然后:块执行多个 spock 交互,而不是为不同的交互集重复相同的交互。
  • DI 并不意味着像 Spring 或 Guice 这样的 DI 框架。它可能只是意味着一个额外的构造函数、设置器或方法参数以及您要注入的相应参数。至于你的测试代码,我不知道你为什么认为你需要一个 interaction {} 块。这是完全没有必要的,甚至可能不会做你认为它做的事情。关于模拟局部变量(类似于 Spock 中的 Groovy 代码的全局模拟),除了使用像 PowerMock 这样的附加框架之外别无他法,正如您已经注意到的那样。
  • 关于“无法更改遗留代码”的更多评论。当然可以!只需进行最小的更改即可使其可模拟测试,而无需使用 PowerMock 之类的框架。在不改变任何其他行为的情况下重构可测试性是一件好事。如果实际上您的意思是“选择不这样做”或“老板不会让我”,请不要说“不能”。有趣的是,有人希望你编写涉及遗留代码的测试,但一开始就不会让你重构遗留代码以实现可测试性。
  • 我什至不明白你为什么要在这里测试交互(方法调用的数量)。测试将与遗留类的内部实现紧密耦合。您最好测试它的行为,即它在注入正常模拟时做正确的事情,并在注入错误模拟时抛出异常。
  • 请注意:我在答案中添加了一个示例,说明我的意思是重构可测试性。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-03-14
  • 2018-11-21
  • 1970-01-01
  • 1970-01-01
  • 2016-01-06
相关资源
最近更新 更多