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

huangapple go评论73阅读模式
英文:

Using a Stub in global setup in Spock

问题

我正在尝试为被测试对象准备一些抽象设置在使用 Stub 时遇到了困难基本上我想要实现的是 - 我有一个类似这样的外观Facade):

    @AllArgsConstructor
    public class Facade {
    
        private final EventBus events;
    
        Mono<String> doSomething() {
            return just("someId").doOnNext(id -> events.push(new ExpectedEvent(id)));
        }
        
        
        /** 还有其他许多在上述测试中需要初始化外观的方法 **/
    }

事件总线EventBus如下

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

以及示例事件Event):

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

我有不同的测试类来针对不同的用例 - 在大多数测试中我不需要与事件总线进行交互所以我希望它是一个简单的实现如下所示

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

但我也希望有可能检查是否已发布了正确的事件因此我也希望能够在需要时注入一个 Stub另一个方面是在测试之前我需要在 setupSpec 方法中添加一些代码来正确设置外观我希望避免在 setup 方法中执行此操作

我会这样看待它

    abstract class AbstractSpec extends Specification {

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

    def setupSpec() {
        /** 在外观上运行不同的方法以准备测试 **/
    }

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

然后所有常规测试只需继承 AbstractSpec 并调用已初始化的外观而在我想要验证 EventBus 调用的类中我会像这样做

    class DerivedSpec extends AbstractSpec {
    
    EventBus eventBus = Mock()

    def "检查是否已发出正确的事件"() {
        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
    }
    }

这是否可以实现上述代码的问题在于我无法在 @Shared 对象上使用 Mock我希望在大多数测试中共享外观的初始化但在某些测试中覆盖初始化使用 EventBus 的模拟来验证与之交互 

在这种情况下Spock 扩展是否有帮助我猜最简单的解决方案是放弃在这个特定的测试中扩展 AbstractSpec并在此单独类中重用 setupSpec 代码但我很好奇是否有其他解决方案
英文:

I am trying to prepare some abstract setup for a tested object and I am stuck when it comes to using a Stub there. Basically what I am trying to achieve - I have a Facade looking like this:

@AllArgsConstructor
public class Facade {
private final EventBus events;
Mono&lt;String&gt; doSomething() {
return just(&quot;someId&quot;).doOnNext(id -&gt; events.push(new ExpectedEvent(id)));
}
/** And many other methods required to initialize the facade in the mentioned test **/
}

with EventBus like:

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

and sample Event:

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

I have different test classes for different use cases - in most of the tests I don't need to interact with the eventBus, so I would like it to be a simple implementation like

event -&gt; just(&quot;No event bus configured&quot;)

But I would also like to have a possibility to check if proper events were published there so I would like also to be able to inject a Stub if needed. Another aspect of it is that I need some code in setupSpec method to properly set up the facade before the tests and I would like to avoid doing it in setup method.

How I would see it:

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 -&gt; just(&quot;No event bus configured&quot;) }
}

Then all the "regular" tests would simple inherit from the AbstractSpec and just call the initialized facade. Whereas in the class where I would like to verify EventBus calls I would have something like this:

class DerivedSpec extends AbstractSpec {
EventBus eventBus = Mock()
def &quot;check if proper event was emited&quot;() {
given:
ExpectedEvent publishedEvent
when:
def someId = facade.doSomething().block()
then:
1 * eventBus.push(_ as ExpectedEvent) &gt;&gt; { ExpectedEvent event -&gt; publishedEvent = event }
publishedEvent.id() == someId
}
EventBus eventBus() {
return eventBus
}
}

Is it somehow achievable? The code above got this problem that I am unable to use Mock with @Shared object. I would like to have common initialization of the facade in most of the tests but override the initialization in some, using mock of EventBus to verify the interactions with it.

Are the Spock extensions helpful in such case? I guess the simplest solution would be to resign from extending the AbstractSpec in this particular test and just reuse the setupSpec code in setup for this single class but I am curious if there is another way to solve it.

答案1

得分: 1

以下是翻译好的内容:

我并不确定你的这种设置是否是解决这个问题的理想方式,它看起来相当牵强,难以理解和维护。我可能会选择不同的测试设计。无论如何,我想限制自己严格地回答你的问题,通过对规范应用最小的更改。

在这里你遇到了引导问题:在AbstractSpec中,你使用了一个@Shared成员,而在许多情况下,这已经是一个反模式,因为这意味着有一个变量在不同特性方法之间携带状态,也就是说,你的测试可能对执行顺序敏感,这是不好的。假设你有多个子类用于这个抽象父类,甚至在不同的测试规范(类)之间可能会有副作用,这更糟糕。但不管怎样,让我把你的共享变量作为一个给定的变量。

现在这个共享变量几乎就像一个静态变量,也就是说,Spock会在任何正常实例变量之前对其进行初始化。因此,尝试通过调用子类重写的方法为其赋值,而该方法尝试赋一个稍后才会初始化的值,会使共享变量获得值null,这在稍后调用对象上的方法时会导致NullPointerException

因此,你需要在子类中将这个模拟对象设置为static。但接下来你会遇到下一个问题:Spock的Mock()调用仅在非静态上下文中起作用。因此,在这里你又遇到了一个先有鸡还是先有蛋的问题。解决方法是创建一个分离的模拟对象,然后在测试执行期间手动将其附加到规范实例上(在这种情况下,@AutoAttach无法起作用)。分离的模拟对象主要是为了被诸如Spring或Guice等DI框架使用而引入的,但也可以独立使用。

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

<!-- language: lang-groovy -->
package de.scrum_master.stackoverflow.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 "检查是否发出了正确的事件"() {
    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
  }
}

请注意,以上翻译的代码内容中的代码部分并未进行翻译。

英文:

I am not really sure if your kind of setup is the ideal way to solve this, it looks quite contrived and difficult to understand and maintain. I would probably choose a different test design. Anyway, I want to limit myself to strictly answering your question by applying minimal changes to your specification.

You are having a bootstrapping problem here: In AbstractSpec you use a @Shared member, which in many cases is an anti pattern already because it means there is one variable carrying state in between feature methods, i.e. your tests could be sensitive to execution order, which is bad. Assuming that you have multiple subclasses for this abstract parent class, there could even be side effects between different test specifications (classes), which is even worse. But be it as it may, let me take your shared variable as a given.

Now this shared variable is almost like a static variable, i.e. Spock will initialise it before any normal instance variables. Hence, trying to assign it a value by calling an method overridden by a subclass and that method trying to assign a value which will only be initialised later makes the shared variable get the value null, which later causes a NullPointerException when trying to call methods upon the object.

So what you need to do is make the mock object in the subclass static. But then you have the next problem: Spock Mock() calls only work in a non-static context. Therefore, you are having the next hen vs. egg problem here. The way to work around it is to create a detached mock and then manually attach it to the specification instance during test execution (@AutoAttach does not work in this case). Detached mocks were mostly introduced in order to be used by DI frameworks such as Spring or Guice, but can be used independently too.

For me your test runs like this:

<!-- language: lang-groovy -->

package de.scrum_master.stackoverflow.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 &quot;check if proper event was emited&quot;() {
given:
ExpectedEvent publishedEvent
new MockUtil().attachMock(eventBus, this)
when:
def someId = facade.doSomething().block()
then:
1 * eventBus.push(_ as ExpectedEvent) &gt;&gt; { ExpectedEvent event -&gt; publishedEvent = event }
publishedEvent.id == someId
}
EventBus eventBus() {
return eventBus
}
}

huangapple
  • 本文由 发表于 2020年8月30日 05:51:28
  • 转载请务必保留本文链接:https://go.coder-hub.com/63652119.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定