如何模拟一个事件处理程序?

4

我为Stash编写了一个事件处理程序,通过消息总线架构发送消息。以下是我的 fedmsgEventListener 类中的一个示例:

@EventListener
public void opened(PullRequestOpenedEvent event)
{
    HashMap<String, Object> message = prExtracter(event);
    String originProjectKey = ((HashMap<String, Object>)message.get("source")).get("project_key").toString();
    String originRepo = ((HashMap<String, Object>)message.get("source")).get("repository").toString();
    String topic = originProjectKey + "." + originRepo + ".pullrequest.opened";
    sendMessage(topic, message);
}

它获取一个事件,从中提取信息,基于事件中的信息构造主题,并调用方法发送消息。我需要为所有这些事件处理程序编写单元测试。

这是运行我尝试实现的第一个测试的类:

import org.junit.Test;
import com.cray.stash.MyPluginComponent;
import com.cray.stash.MyPluginComponentImpl;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

public class MyComponentUnitTest
{
    @Test
    public void testMyName()
    {
        MyPluginComponent component = new MyPluginComponentImpl(null);       
        assertTrue(component.openPullRequest().contains(".pullrequest.opened"));
    }
}

接下来是测试调用的类和方法:

import com.atlassian.sal.api.ApplicationProperties;
import com.atlassian.stash.event.pull.*;
import org.mockito.Mock;
import static org.mockito.Mockito.*;

public class MyPluginComponentImpl implements MyPluginComponent
{
    @Mock private PullRequestEvent event;
    @Mock private PullRequestOpenedEvent opened;
    @Mock private FedmsgEventListener fedmsgEventListener;

    public MyPluginComponentImpl(ApplicationProperties applicationProperties)
    {
        this.applicationProperties = applicationProperties;
    }

    public String openPullRequest()
    {
        fedmsgEventListener.opened(opened);
        return fedmsgEventListener.getTopic();
    }

}

目前,由于fedmsgEventListenerPullRequestEvent都是模拟对象且值为空,因此该方法会抛出NullPointerException

这是测试该场景的最佳方法吗?从高层次上看,我想要做的就是触发事件,查看主题是否更改为包含特定字符串的字符串。


@durron597 好的,我尝试以更好的方式描述它。如果有改进请告诉我。 - Scott James Walter
1个回答

6
抱歉,你完全错误地使用了Mockito。首先,如果不使用initMocks或MockitoJUnitRunner,@Mock是无法工作的,但我也不会那样做。模拟对象不是null;你应该能够在模拟对象上调用方法;在你的情况下,你没有初始化/创建模拟对象,这就是它们为什么为null的原因。
首先,确定你要测试的类。这里看起来是FedmsgEventListener。然后,使用模拟对象和数据结构与该类的一个真实实例进行交互,而不是具有依赖关系等真实对象。注意,我在这里使用的是Hamcrest 1.3。
基于模拟的测试由三个阶段组成。
  1. 创建 - 创建你的模拟对象,并且指定当模拟对象发生交互时,会做些什么。
  2. 交互 - 以你试图测试的方式与对象进行交互。
  3. 验证 - 使用 Mockito.verify 和 JUnit/Hamcrest 的 assert 方法来确保事情按照你的预期工作。

你可以像这样做:

import static org.mockito.Mockito.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;

private HashMap<String, Object> createMessageDetails(String project_key, String repository) {
  HashMap<String, Object> details = new HashMap<>();
  details.put("project_key", project_key);
  details.put("repository", repository);
  return details;
}

public class FedmsgEventListenerTest {
  @Test
  public void testOpened() {
    // when
    PullRequestOpenedEvent event = mock(PullRequestOpenedEvent.class);
    when(event.someMethodForPrExtracterYouHaventShownMe()).thenReturn(createMessageDetails("myKey", "myRepo"));

    // then
    FedmsgEventListener listener = new FedmsgEventListener();
    listener.opened(event);

    // verify
    assertThat(event.getTopic(), containsString(".pullrequest.opened"));
    verify(event).someMethodForPrExtracterYouHaventShownMe();
  }
}

这份代码可能不完全符合你的需求,但是你并没有给我足够的测试代码,所以我无法完全准确地理解。不过,我认为这应该足够让你开始了。
作为旁注,如果您无法使用模拟依赖项创建实际的类实例,则这是一种代码气味,应该重构您的代码。这就是静态方法如此糟糕的原因之一,因为如果您的代码通过静态方法访问全局状态,则必须使用静态方法设置全局状态。使您的类能够使用模拟依赖项工作,将它们作为参数传递给构造函数,使用when指定模拟行为,然后断言/验证结果。

感谢您详尽的回答。我的类FedmsgEventListener似乎在观察到事件时被实例化。此时,许多与事件相关的参数都会传递给该类的构造函数。我可能可以模拟这些参数,但我不确定那会带来什么副作用。无论如何,感谢您提供有关使用模拟的基本说明。 - Scott James Walter
@ScottJamesWalter 很高兴我能帮到你。如果出现这种情况,那么你的代码可能过于紧密耦合在一起,需要进行重构。请参考单一职责原则 - durron597

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接