Как частично смоделировать метод, который генерирует исключения, используя Mockito?

Полезно протестировать обработку исключений. В этом конкретном случае у меня есть экстрактор, который будет выполнять определенную задачу, когда возникает исключение при демаршалировании определенного класса.

Пример кода

Ниже приведен упрощенный пример кода. Производственный вариант намного сложнее.

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;
        }
    }
}

Вещи, которые я пробовал

Вот список вещей, которые я пробовал. Для краткости я не заполнил все мирские детали.

Шпионаж

Следующий метод ничего не делает, и исключение не генерируется. Я не уверен, почему.

@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));
}

Насмешка

Следующее не сработало, потому что частичные макеты, похоже, плохо работают с методами, которые генерируют исключения.

@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));
}

ТогдаОтветить

Это работает, но я не уверен, что это правильный способ сделать это:

@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));
}

Вопрос

Хотя метод thenAnswer работает, он не кажется правильным решением. Как правильно выполнить частичный макет для этой ситуации?


person Pete    schedule 01.05.2015    source источник
comment
Меня беспокоит то, что вы смешиваете издевательский маршаллер с новым маршаллером. Не могли бы вы показать нам, как вы пытались пройти тест (полностью) со шпионом и макетом? Мое внутреннее чувство подсказывает мне, что вам, вероятно, нужен только макет, но я хотел бы увидеть другой подход для полноты.   -  person Makoto    schedule 01.05.2015
comment
@Makoto Я обновил вопрос урезанной тестовой версией, которая воспроизводит проблемы, которые я вижу в производственном коде. Надеюсь, это немного лучше объяснит ситуацию.   -  person Pete    schedule 02.05.2015


Ответы (1)


Я не вполне уверен, к чему вы клоните, говоря о насмешках и шпионаже, но вам действительно нужно поиздеваться только здесь.

Во-первых, по какой-то причине я столкнулся с несколькими препятствиями, когда пробовал ваши макеты. Я считаю, что это было связано с вызовом spy, который был каким-то образом перепутан. В конце концов я преодолел их, но я хотел получить что-то простое.

Затем я заметил что-то не так с тем, как вы шпионили (основа моего подхода):

MyHelper spied = spy(fixture.adapter);

Это означает, что вы хотите, чтобы экземпляр MyHelper был замаскирован, а не шпионил. Хуже всего то, что даже если бы этот объект был полностью гидратирован, он не был бы правильно введен, поскольку вы не переназначили его тестовому объекту (который, как я полагаю, fixture).

Я предпочитаю использовать MockitoJUnitRunner, чтобы помочь с внедрением имитируемых экземпляров, и оттуда я создаю основу того, что мне действительно нужно имитировать.

Есть только один имитируемый экземпляр, а затем тестовый объект, и это объявление гарантирует, что они оба созданы и внедрены:

@RunWith(MockitoJUnitRunner.class)
public class ExampleTest {

    @Mock
    private MyHelper adapter;

    @InjectMocks
    private MyService fixture;
}

Идея состоит в том, что вы внедряете макет в прибор. Вам не обязательно использовать это — вы можете использовать стандартные установщики в объявлении @Before, но я предпочитаю это, поскольку это значительно сокращает шаблонный код, который вам нужно написать, чтобы заставить mock работать.

Теперь осталось сделать только одно изменение: удалить шпионский экземпляр и заменить его предыдущее использование фактическим макетом.

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

Со всем поднятым кодом это проходит:

@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));
    }
}

Не желая оставлять вопрос/прецедент незавершенным, я обошел круг и заменил тест внутренними классами, и он тоже отлично работает:

@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));
    }
}
person Makoto    schedule 02.05.2015
comment
Спасибо за ваше время. У меня был вопрос, я думаю, что изначально настроил все как таковое, потому что присваивал обработанное значение экземпляру Thing. Мой коллега сказал, что проблема была вызвана тестированием двух вещей в одном тесте. Я думаю, что издевательство над адаптером - правильный путь. Еще раз спасибо за вашу помощь! - person Pete; 03.05.2015