Как частично смоделировать метод, который выбрасывает исключения, используя 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));
}
ThenAnswer
Это работает, но я не уверен, что это правильный способ сделать это:
@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
метод работает, это не кажется правильным решением. Как правильно выполнить частичное макетирование в этой ситуации?
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
декларации, но я предпочитаю это, так как это значительно сокращает шаблонный код, который вы должны написать, чтобы заставить насмешку работать.
Теперь есть только одно изменение: удалите экземпляр шпиона и замените его предыдущее использование фактическим макетом.
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));
}
}