为什么void方法在模拟对象上得到NullPointerException ?



我正在为我的java应用程序编写单元测试,该应用程序分析提交到Stash,这是一个类似于Github的atlassian应用程序。

我要测试的方法是:

public List<Message> processEvent(RepositoryRefsChangedEvent event) {
    ArrayList<Message> commitList = new ArrayList<Message>();
    for (RefChange refChange : event.getRefChanges()) {
        LOGGER.info("checking ref change refId={} fromHash={} toHash={} type={}", refChange.getRefId(), refChange.getFromHash(),
                refChange.getToHash(), refChange.getType());
        if (refChange.getRefId().startsWith(REF_BRANCH)) {
            if (refChange.getType() == RefChangeType.ADD && isDeleted(refChange)) {
                LOGGER.info("Deleted a ref that never existed. This shouldn't ever occur.");
            }
            else if (isDeleted(refChange) || isCreated(refChange)) {
                branchCreation(refChange, event.getRepository(), commitList);
            }
            else {
                sepCommits.findCommitInfo(refChange, event.getRepository(), commitList);
            }
        }
        else {
            refNotProcessed(refChange);
        }
    }
    return commitList;
}

我试图确保,如果我有一个git笔记提交,处理被忽略,refNotProcessed(..)被调用。

幸运的是,我能够相对容易地解决这个问题,并得出以下解决方案:

@RunWith (MockitoJUnitRunner.class)
public class RefChangEventTest {
    @Mock RefChange ref;
    @Mock RepositoryRefsChangedEvent refsChangedEvent;
    @Mock Repository repo;
    @Mock ApplicationPropertiesService appService;
    @Mock SEPCommits sepCommits;
    @Spy SEPRefChangeEventImpl sepRefChangeEvent = new SEPRefChangeEventImpl(sepCommits, appService);
    @Before
    public void testSetup() {
        Collection<RefChange> refList = new ArrayList<RefChange>(1);
        refList.add(ref);
        when(refsChangedEvent.getRefChanges()).thenReturn(refList);
        when(refsChangedEvent.getRepository()).thenReturn(repo);
    }
    @Test
    public void gitNotesAreIgnored() throws Exception {
        when(ref.getRefId()).thenReturn("refs/notes/foo");
        when(ref.getFromHash()).thenReturn("da69d7e202d7f66cba01c6f4030bd5975adbf200");
        when(ref.getToHash()).thenReturn("da69d7e202d7f66cba01c6f4030bd5975adbf201");
        doNothing().when(sepCommits).findCommitInfo(any(RefChange.class), any(Repository.class), any(ArrayList.class));
        sepRefChangeEvent.processEvent(refsChangedEvent);
        verify(sepRefChangeEvent, times(1)).refNotProcessed(ref);
    }
在此之后,我想看看如果我将ref名称更改为refs/heads/foo之类的东西,我的单元测试是否会因为正确的原因而失败。我希望看到这样的内容:expected 1 execution of refNotProcessed but was not run at all

得到:

java.lang.NullPointerException
at com.cray.stash.SEPRefChangeEventImpl.processEvent(SEPRefChangeEventImpl.java:62)
at ut.com.isroot.stash.plugin.RefChangEventTest.gitNotesAreIgnored(RefChangEventTest.java:48)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
at org.mockito.internal.runners.JUnit45AndHigherRunnerImpl.run(JUnit45AndHigherRunnerImpl.java:37)
at org.mockito.runners.MockitoJUnitRunner.run(MockitoJUnitRunner.java:62)
at org.junit.runner.JUnitCore.run(JUnitCore.java:160)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:117)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:42)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:253)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:84)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

指向对sepCommits.findCommitInfo(..)的调用。默认情况下,具有void签名的方法在模拟调用它们时不做任何事情。这正是我想让它做的。我希望它被调用,但不做任何事情,只是记录与sepCommits交互的事实。为什么会发生NPE ?

这里是人们要求的更多方法:

public SEPRefChangeEventImpl(SEPCommits sepCommits, ApplicationPropertiesService appService) {
    this.sepCommits = sepCommits;
    try {
        endpoint = appService.getPluginProperty("plugin.fedmsg.events.relay.endpoint");
    } catch (Exception e) {
        LOGGER.error("Failed to retrieve propertiesn" + e);
    }
    if (endpoint == null) {
        endpoint = "tcp://some.web.address"
    }
}

public void refNotProcessed(RefChange refChange) {
    LOGGER.info("This type of refChange is not supported.n refId={} fromHash={} toHash={} type={}", refChange.getRefId(), refChange.getFromHash(),
            refChange.getToHash(), refChange.getType());
}
public void findCommitInfo(RefChange ref, Repository repo, ArrayList<Message> commitList) {
    Page<Commit> commits = getChangeset(repo, ref);
    for (Commit commit : commits.getValues()) {
        String topic = topicPrefix + repo.getProject().getKey() + "." + repo.getName() + ".commit";
        Message message = new Message(getInfo(commit, ref), topic);
        commitList.add(message);
    }
}

从我在您的设置中看到的,sepCommits是类中的依赖项,包含processEvent()方法。

您必须在sepRefChangeEvent变量中注入您在测试中创建的模拟。通常,这是通过在构造期间将其作为参数传递或通过setter方法来完成的。我在您的测试类中没有看到这样的代码。我认为你实际上击中了一个真实的实例,而不是那里的模拟,这是导致异常的

我认为Mockito间谍不是一种自然的行为,因为你的类既是mock又是要测试的类。
在遗留代码中,这可能是可以接受的,但在新代码中,这是一个遗憾。

此外,就你的情况而言,我认为你不需要监视。
您的逻辑是:如果ref name输入与REF_BRANCH匹配,则执行处理,否则不执行任何操作。因此,您应该需要检查在可接受的测试中执行处理(调用验证mock),而在不可接受的测试中不执行处理(未调用验证mock)。

       // You do the processing
       if (refChange.getRefId().startsWith(REF_BRANCH)) {
            if (refChange.getType() == RefChangeType.ADD && isDeleted(refChange)) {
                LOGGER.info("Deleted a ref that never existed. This shouldn't ever occur.");
            }
            else if (isDeleted(refChange) || isCreated(refChange)) {
                branchCreation(refChange, event.getRepository(), commitList);
            }
            else {
                sepCommits.findCommitInfo(refChange, event.getRepository(), commitList);
            }
        }
        // You do nothing
        else {
            refNotProcessed(refChange);
        }

断言被测试类的另一个公共方法被称为验收测试不是验收测试。

如果你真的想测试它,另一个解决方案是引入一个新的类来移动public method refNotProcessed。您可以保留这个类来控制输入流,并将其分派给处理类中的处理方法:

       // You do the processing
       if (refChange.getRefId().startsWith(REF_BRANCH)) {
            if (refChange.getType() == RefChangeType.ADD && isDeleted(refChange)) {
                LOGGER.info("Deleted a ref that never existed. This shouldn't ever occur.");
            }
            else if (isDeleted(refChange) || isCreated(refChange)) {
                processClass.branchCreation(refChange, event.getRepository(), commitList);
            }
            else {
                sepCommits.findCommitInfo(refChange, event.getRepository(), commitList);
            }
        }
        // You do nothing
        else {
            processClass.refNotProcessed(refChange);
        }

这是我的解决方案:

@RunWith (MockitoJUnitRunner.class)
public class RefChangEventTest {
    @Mock RefChange ref;
    @Mock RepositoryRefsChangedEvent refsChangedEvent;
    @Mock ApplicationPropertiesService appService;
    @Mock ArrayList<Message> mockList;
    @Mock RepositoryService repositoryService;
    @Mock RefService refService;
    @Mock CommitService commitService;
    @Mock SecurityService securityService;
    @Mock Repository repo;
    SEPCommits sepCommits = mock(SEPCommits.class, RETURNS_DEEP_STUBS);
    @Spy SEPRefChangeEventImpl sepRefChangeEvent = new SEPRefChangeEventImpl(sepCommits, appService);
    @Before
    public void testSetup() {
        Collection<RefChange> refList = new ArrayList<RefChange>(1);
        refList.add(ref);
        when(refsChangedEvent.getRefChanges()).thenReturn(refList);
        when(refsChangedEvent.getRepository()).thenReturn(repo);
    }
    @Test
    public void gitNotesAreIgnored() throws Exception {
        when(ref.getRefId()).thenReturn("refs/notes/foo");
        when(ref.getFromHash()).thenReturn("da69d7e202d7f66cba01c6f4030bd5975adbf200");
        when(ref.getToHash()).thenReturn("da69d7e202d7f66cba01c6f4030bd5975adbf201");
        sepRefChangeEvent.processEvent(refsChangedEvent);
        verifyZeroInteractions(sepCommits);
    }

区别在于如何模拟sepCommits,这是RETURN_DEEP_STUBS。我避免完全初始化sepCommits的原因是,这样我需要在findCommitInfo(..)内部调用大量的方法。我只是想确保那个方法要么被调用要么不被调用。我不确定RETURN_DEEP_STUBS是否是一个很好的解决方案,但它对我有用。

相关内容

  • 没有找到相关文章

最新更新