在 Spock 的全局设置中使用存根

Using a Stub in global setup in Spock

我正在尝试为测试对象准备一些抽象设置,但当涉及到在那里使用存根时,我被卡住了。基本上我想要实现的目标 - 我有一个看起来像这样的外观:

@AllArgsConstructor
public class Facade {

    private final EventBus events;

    Mono<String> doSomething() {
        return just("someId").doOnNext(id -> events.push(new ExpectedEvent(id)));
    }
    
    
    /** And many other methods required to initialize the facade in the mentioned test **/
}

像这样使用 EventBus:

public interface EventBus {
    void push(Event event);
}

和样本事件:

@Value
public class ExpectedEvent implements Event {
    String id;
}

对于不同的用例,我有不同的测试 classes - 在大多数测试中,我不需要与 eventBus 交互,所以我希望它是一个简单的实现,比如 [=16] =]

event -> just("No event bus configured")

但我也希望能够检查那里是否发布了适当的事件,因此我也希望能够在需要时注入存根。它的另一个方面是我需要在 setupSpec 方法中使用一些代码在测试之前正确设置外观,我想避免在 setup 方法中这样做。

我怎么看:

abstract class AbstractSpec extends Specification {

@Shared
Facade facade = new Facade(
        eventBus())

def setupSpec() {
    /** run different methods on the facade to prepare it for the tests **/
}

EventBus eventBus() {
    return { event -> just("No event bus configured") }
}

然后所有“常规”测试将简单地从 AbstractSpec 继承并调用初始化的外观。而在我想验证 EventBus 调用的 class 中,我会有这样的东西:

class DerivedSpec extends AbstractSpec {

EventBus eventBus = Mock()

def "check if proper event was emited"() {
    given:
        ExpectedEvent publishedEvent

    when:
        def someId = facade.doSomething().block()

    then:
        1 * eventBus.push(_ as ExpectedEvent) >> { ExpectedEvent event -> publishedEvent = event }
        publishedEvent.id() == someId
}

EventBus eventBus() {
    return eventBus
}
}

是否可以通过某种方式实现?上面的代码遇到了这个问题,我无法将 Mock 与 @Shared 对象一起使用。我希望在大多数测试中对外观进行公共初始化,但在某些测试中覆盖初始化,使用 EventBus 的模拟来验证与它的交互。

Spock 扩展在这种情况下有用吗?我想最简单的解决方案是放弃在这个特定测试中扩展 AbstractSpec,而只是在设置中为这个单一 class 重复使用 setupSpec 代码,但我很好奇是否有另一种方法可以解决它。

我不太确定您的设置是否是解决此问题的理想方法,它看起来很做作并且难以理解和维护。我可能会选择不同的测试设计。无论如何,我想限制自己通过对您的规范进行最小的更改来严格回答您的问题。

你在这里遇到了引导问题:在 AbstractSpec 中你使用了一个 @Shared 成员,在很多情况下它已经是一种反模式,因为它意味着在特征之间有一个变量携带状态方法,即您的测试可能对执行顺序敏感,这是不好的。假设你有多个 subclasses 这个抽象父 class,不同的测试规范 (classes) 之间甚至可能存在副作用,这更糟。但无论如何,让我把你的共享变量作为给定的。

现在这个共享变量几乎就像一个静态变量,即 Spock 会在任何普通实例变量之前初始化它。因此,尝试通过调用一个被 subclass 覆盖的方法来为其赋值,而该方法试图赋值一个只会在稍后初始化的值,这使得共享变量获得值 null,稍后尝试在对象上调用方法时导致 NullPointerException

所以你需要做的是在子classstatic中创建模拟对象。但是你会遇到下一个问题:Spock Mock() 调用只能在 non-static 上下文中工作。因此,您在这里遇到了下一个母鸡与鸡蛋的问题。解决它的方法是创建一个 detached mock,然后在测试执行期间手动将其附加到规范实例(@AutoAttach 在这种情况下不起作用)。引入分离模拟主要是为了供 Spring 或 Guice 等 DI 框架使用,但也可以独立使用。

对我来说,你的测试是这样运行的:

package de.scrum_master.Whosebug.q63652119

import org.spockframework.mock.MockUtil
import spock.mock.DetachedMockFactory

class DerivedSpec extends AbstractSpec {
  static mockFactory = new DetachedMockFactory()
  static EventBus eventBus = mockFactory.Mock(EventBus)

  def "check if proper event was emited"() {
    given:
    ExpectedEvent publishedEvent
    new MockUtil().attachMock(eventBus, this)

    when:
    def someId = facade.doSomething().block()

    then:
    1 * eventBus.push(_ as ExpectedEvent) >> { ExpectedEvent event -> publishedEvent = event }
    publishedEvent.id == someId
  }

  EventBus eventBus() {
    return eventBus
  }
}