Search code examples
javaunit-testingmockitopartial-mocks

How to partial mock a method that throws exceptions using Mockito?


It's useful to test exception handling. In this specific case, I have a extractor that will do a specific task when an exception is thrown while unmarshaling a specific class.

Example Code

Below is a simplified example of the code. The production version is much more complicated.

public class Example {
    public static enum EntryType {
        TYPE_1,
        TYPE_2
    }

    public static class Thing {
        List<String> data = new ArrayList<String>();
        EnumSet<EntryType> failedConversions = EnumSet.noneOf(EntryType.class);
    }

    public static class MyHelper {
        public String unmarshal(String input) throws UnmarshalException {
            // pretend this does more complicated stuff
            return input + " foo "; 
        }
    }

    public static class MyService {

        MyHelper adapter = new MyHelper();

        public Thing process() {
            Thing processed = new Thing();

            try {
                adapter.unmarshal("Type 1");
            } catch (UnmarshalException e) {
                processed.failedConversions.add(EntryType.TYPE_1);
            }

            // do some stuff

            try {
                adapter.unmarshal("Type 2");
            } catch (UnmarshalException e) {
                processed.failedConversions.add(EntryType.TYPE_2);
            }

            return processed;
        }
    }
}

Things I've Tried

Here's a list of things I've tried. For brevity, I haven't filled in all the mundane details.

Spying

The following method doesn't do anything and the exception doesn't throw. I'm not sure why.

@Test
public void shouldFlagFailedConversionUsingSpy()
        throws Exception {
    MyHelper spied = spy(fixture.adapter);
    doThrow(new UnmarshalException("foo")).when(spied).unmarshal(
            Mockito.eq("Type 1"));

    Thing actual = fixture.process();
    assertEquals(1, actual.failedConversions.size());
    assertThat(actual.failedConversions.contains(EntryType.TYPE_1), is(true));
}

Mocking

The following didn't work because partial mocks don't seem to play well with methods that throw exceptions.

@Test
public void shouldFlagFailedConversionUsingMocks()
        throws Exception {
    MyHelper mockAdapter = mock(MyHelper.class);
    when(mockAdapter.unmarshal(Mockito.anyString())).thenCallRealMethod();
    when(mockAdapter.unmarshal(Mockito.eq("Type 2"))).thenThrow(
            new UnmarshalException("foo"));

    Thing actual = fixture.process();
    assertEquals(1, actual.failedConversions.size());
    assertThat(actual.failedConversions.contains(EntryType.TYPE_2), is(true));
}

ThenAnswer

This works, but I'm not sure if it's the proper way to do this:

@Test
public void shouldFlagFailedConversionUsingThenAnswer() throws Exception {
    final MyHelper realAdapter = new MyHelper();
    MyHelper mockAdapter = mock(MyHelper.class);
    fixture.adapter = mockAdapter;

    when(mockAdapter.unmarshal(Mockito.anyString())).then(
            new Answer<String>() {

                @Override
                public String answer(InvocationOnMock invocation)
                        throws Throwable {
                    Object[] args = invocation.getArguments();
                    String input = (String) args[0];
                    if (input.equals("Type 1")) {
                        throw new UnmarshalException("foo");
                    }
                    return realAdapter.unmarshal(input);
                }

            });

    Thing actual = fixture.process();
    assertEquals(1, actual.failedConversions.size());
    assertThat(actual.failedConversions.contains(EntryType.TYPE_1), is(true));
}

Question

Although the thenAnswer method works, it doesn't seem to be the proper solution. What is the correct way to perform a partial mock for this situation?


Solution

  • I'm not quite sure what you were getting at with the mocking and the spying, but you only really need to mock here.

    First, I ran into a few snags when trying your mocks out for whatever reason. I believe this had to do with the spy call which was messed up in some way. I did eventually overcome these, but I wanted to get something simple to pass.

    Next, I did notice something off with the way you were spying (the basis of my approach):

    MyHelper spied = spy(fixture.adapter);
    

    This implies that you want an instance of MyHelper mocked out, not spied. The worst part is that even if this object were fully hydrated, it wouldn't be properly injected since you haven't reassigned it to the test object (which I presume is fixture).

    My preference is to use the MockitoJUnitRunner to help with the injection of mocked instances, and from there I build up a basis of what it is I actually need to mock.

    There's only one mocked instance and then the test object, and this declaration will ensure that they're both instantiated and injected:

    @RunWith(MockitoJUnitRunner.class)
    public class ExampleTest {
    
        @Mock
        private MyHelper adapter;
    
        @InjectMocks
        private MyService fixture;
    }
    

    The idea is that you're injecting your mock into the fixture. You don't have to use this - you could use standard setters in a @Before declaration, but I prefer this since it greatly reduces the boilerplate code you have to write to get mocking to work.

    Now there's only one change to be made: remove the spy instance and replace its previous usage with the actual mock.

    doThrow(new UnmarshalException("foo")).when(adapter).unmarshal(eq("Type 1"));
    

    With all of the code hoisted, this passes:

    @RunWith(MockitoJUnitRunner.class)
    public class ExampleTest {
        @Mock
        private MyHelper adapter;
    
        @InjectMocks
        private MyService fixture;
    
        @Test
        public void shouldFlagFailedConversionUsingSpy()
                throws Exception {
    
            doThrow(new UnmarshalException("foo")).when(adapter).unmarshal(eq("Type 1"));
    
            Thing actual = fixture.process();
            assertEquals(1, actual.failedConversions.size());
            assertThat(actual.failedConversions.contains(Example.EntryType.TYPE_1), is(true));
        }
    }
    

    Not being one to want to leave the question/use case incomplete, I circled around and replaced the test with the inner classes, and it works fine too:

    @RunWith(MockitoJUnitRunner.class)
    public class ExampleTest {
        @Mock
        private Example.MyHelper adapter;
    
        @InjectMocks
        private Example.MyService fixture;
    
        @Test
        public void shouldFlagFailedConversionUsingSpy()
                throws Exception {
    
            doThrow(new UnmarshalException("foo")).when(adapter).unmarshal(eq("Type 1"));
    
            Example.Thing actual = fixture.process();
            assertEquals(1, actual.failedConversions.size());
            assertThat(actual.failedConversions.contains(Example.EntryType.TYPE_1), is(true));
        }
    }