Noel Yap
Noel Yap

Reputation: 19788

How to fix `UnfinishedStubbingException`?

I have the following:

public class EnvWebEndpointExtensionEnvironmentPostProcessorTests {
  @Rule
  public ExpectedException thrown = ExpectedException.none();

  @Rule
  public MockitoRule rule = MockitoJUnit.rule();

  final EnvWebEndpointExtensionEnvironmentPostProcessor postProcessor = new EnvWebEndpointExtensionEnvironmentPostProcessor();

  @Mock
  ConfigurableEnvironment environmentMock;

  @Mock
  MutablePropertySources propertySourcesMock;

  @Test
  public void shouldAddPropertySource() {
    final MutablePropertySources propertySources = new MutablePropertySources();

    doReturn(propertySources) // line 40
        .when(environmentMock).getPropertySources();

    postProcessor.postProcessEnvironment(environmentMock, null);

    assertNotNull(propertySources.get("actuators-defaults"));
  }

  @Test
  public void shouldThrowExceptionOnFailingToAddLaptopPropertySource() {
    thrown.expect(RuntimeException.class);

    final MutablePropertySources propertySourcesReal = new MutablePropertySources();

    doReturn(propertySourcesReal)
        .when(environmentMock).getPropertySources();

    doReturn(true)
        .when(environmentMock).acceptsProfiles("laptop");
    doReturn(propertySourcesMock)
        .when(environmentMock).getPropertySources();
    doThrow(IOException.class) // line 61
        .when(propertySourcesMock).addBefore("actuators-defaults", any(ResourcePropertySource.class));

    postProcessor.postProcessEnvironment(environmentMock, null);
  }
}

When the tests are run individually, they pass but when they're both run, shouldAddPropertySource fails with:

org.mockito.exceptions.misusing.UnfinishedStubbingException: 
Unfinished stubbing detected here:
-> at com.netflix.springboot.actuators.EnvWebEndpointExtensionEnvironmentPostProcessorTests.shouldThrowExceptionOnFailingToAddLaptopPropertySource(EnvWebEndpointExtensionEnvironmentPostProcessorTests.java:61)

E.g. thenReturn() may be missing.
Examples of correct stubbing:
    when(mock.isOk()).thenReturn(true);
    when(mock.isOk()).thenThrow(exception);
    doThrow(exception).when(mock).someVoidMethod();
Hints:
 1. missing thenReturn()
 2. you are trying to stub a final method, which is not supported
 3: you are stubbing the behaviour of another mock inside before 'thenReturn' instruction if completed


    at com.netflix.springboot.actuators.EnvWebEndpointExtensionEnvironmentPostProcessorTests.shouldAddPropertySource(EnvWebEndpointExtensionEnvironmentPostProcessorTests.java:40)
    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:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.mockito.internal.junit.JUnitRule$1.evaluateSafely(JUnitRule.java:52)
    at org.mockito.internal.junit.JUnitRule$1.evaluate(JUnitRule.java:43)
    at org.junit.rules.ExpectedException$ExpectedExceptionStatement.evaluate(ExpectedException.java:239)
    at org.junit.rules.RunRules.evaluate(RunRules.java:20)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)

Considering the information I've found and the behavior above, Mockito is storing some static state but the level of my understanding isn't deep enough to figure out a fix for the above. What's the right fix and, in addition, the explanation for the fix?

Upvotes: 0

Views: 894

Answers (2)

Jeff Bowman
Jeff Bowman

Reputation: 95654

JB Nizet correctly diagnosed the root cause in his answer: When you use matchers for one argument, you have to use matchers for all arguments.

My hunch is that Mockito is correctly throwing InvalidUseOfMatchersException, which descends from RuntimeException, so your test erroneously passes without exercising your system-under-test. This is an important reason not to catch RuntimeException indiscriminately, especially at the top of a test method. This may also be a reason to use assertThrows or a try { methodUnderTest(); fail(); } catch (YourSpecificException expected) {} idiom.

If that's the case, you're seeing that specific exception because your test runner is calling your tests in shouldThrow, shouldAdd order in the same VM and Mockito keeps its matcher state in a static ThreadLocal that may survive between tests. If that theory is correct, then the InvalidUseOfMatchersException happens before Mockito can store the expectation from line 61, leaving the stubbing on line 61 technically unfinished. Because Mockito doesn't know when one test ends and another begins, it can't reset its state, so Mockito can only detect this case the next time you interact with Mockito (on line 40).

You could improve your experience by calling Mockito.validateMockitoUsage() in an @After method (or by using MockitoRule or MockitoJUnitRunner).

Upvotes: 1

JB Nizet
JB Nizet

Reputation: 691785

If you use a matcher as an argument of a stubbed method call, all arguments must be matchers. So you need to replace "actuators-defaults" by eq("actuators-defaults").

I'm not sure why that throws that exception, though.

Upvotes: 2

Related Questions