英文:
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<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 **/
}
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 -> just("No event bus configured")
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 -> just("No event bus configured") }
}
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 "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
}
}
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 "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
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论