Как захватить список определенного типа с помощью mockito

301

Есть ли способ захвата списка определенного типа с помощью mockitos ArgumentCaptore. Это не работает:

ArgumentCaptor<ArrayList<SomeType>> argument = ArgumentCaptor.forClass(ArrayList.class);
Андреас Кёберле
источник
8
Я считаю, что это ужасная идея - использовать конкретную реализацию списка здесь ( ArrayList). Вы всегда можете использовать Listинтерфейс, и если вы хотите представить факт, что он ковариантен, то вы можете использовать extends:ArgumentCaptor<? extends List<SomeType>>
tenhi

Ответы:

533

Вложенной непатентованной проблемы можно избежать с помощью аннотации @Captor :

public class Test{

    @Mock
    private Service service;

    @Captor
    private ArgumentCaptor<ArrayList<SomeType>> captor;

    @Before
    public void init(){
        MockitoAnnotations.initMocks(this);
    }

    @Test 
    public void shouldDoStuffWithListValues() {
        //...
        verify(service).doStuff(captor.capture()));
    }
}
crunchdog
источник
70
Я предпочитаю использовать MockitoAnnotations.initMocks(this)в @Beforeметоде, а не использовать бегуна, который исключает возможность использовать другого бегуна. Тем не менее, +1, спасибо за указание на аннотацию.
Джон Б,
4
Не уверен, что этот пример завершен. Я получаю ... Ошибка: (240, 40) Java: переменная Captor, возможно, не был инициализирован. Мне нравится ответ Тенши ниже
Майкл Даусманн
1
Я столкнулся с той же проблемой и нашел этот пост, который мне немного помог: blog.jdriven.com/2012/10/… . Он включает в себя этап использования MockitoAnnotations.initMocks после того, как вы добавили аннотацию в свой класс. Одна вещь, которую я заметил, это то, что вы не можете иметь ее в локальной переменной.
SlopeOak
1
@ chamzz.dot ArgumentCaptor <ArrayList <SomeType >> captor; уже захватывает массив "SomeType" <- это определенный тип, не так ли?
Мигель Р. Сантелла
1
Я обычно предпочитаю List вместо ArrayList в объявлении Captor: ArgumentCaptor <List <SomeType >> captor;
Мигель Р. Сантелла
146

Да, это общая проблема дженериков, а не специфическая для мокито.

Для объекта нет объекта класса ArrayList<SomeType>, и, таким образом, вы не можете безопасно передавать этот объект методу, требующему a Class<ArrayList<SomeType>>.

Вы можете привести объект к нужному типу:

Class<ArrayList<SomeType>> listClass =
              (Class<ArrayList<SomeType>>)(Class)ArrayList.class;
ArgumentCaptor<ArrayList<SomeType>> argument = ArgumentCaptor.forClass(listClass);

Это выдаст несколько предупреждений о небезопасных приведениях, и, конечно, ваш ArgumentCaptor не сможет по-настоящему провести различие между ArrayList<SomeType>и ArrayList<AnotherType>без проверки элементов.

(Как уже упоминалось в другом ответе, хотя это общая проблема общего характера, для аннотации существует специфическое для Mockito решение проблемы безопасности типов @Captor. Он по-прежнему не может различить ArrayList<SomeType>и ArrayList<OtherType>.)

Редактировать:

Взгляните также на комментарий Тэнши . Вы можете изменить исходный код с Paŭlo Ebermann на этот (гораздо проще)

final ArgumentCaptor<List<SomeType>> listCaptor
        = ArgumentCaptor.forClass((Class) List.class);
Пауло Эберманн
источник
49
Пример, который вы показали, может быть упрощен, основываясь на том факте, что java делает вывод типа для вызовов статического метода:ArgumentCaptor<List<SimeType>> argument = ArgumentCaptor.forClass((Class) List.class);
tenhi
4
Чтобы отключить использование предупреждений о непроверенных или небезопасных операциях , используйте @SuppressWarnings("unchecked")аннотацию над строкой определения захвата аргумента. Кроме того, приведение к Classизлишним.
MRTS
1
Приведение к Classне является лишним в моих тестах.
Вим Deblauwe
16

Если вы не боитесь старой семантики в стиле java (не являющейся типичной безопасностью), это также работает и достаточно просто:

ArgumentCaptor<List> argument = ArgumentCaptor.forClass(List.class);
verify(subject.method(argument.capture()); // run your code
List<SomeType> list = argument.getValue(); // first captured List, etc.
rogerdpack
источник
2
Вы можете добавить @SuppressWarnings ("rawtypes") перед объявлением, чтобы отключить предупреждения.
pkalinow
9
List<String> mockedList = mock(List.class);

List<String> l = new ArrayList();
l.add("someElement");

mockedList.addAll(l);

ArgumentCaptor<List> argumentCaptor = ArgumentCaptor.forClass(List.class);

verify(mockedList).addAll(argumentCaptor.capture());

List<String> capturedArgument = argumentCaptor.<List<String>>getValue();

assertThat(capturedArgument, hasItem("someElement"));
kkmike999
источник
4

Основываясь на комментариях @ tenhi и @ pkalinow (также благодарности @rogerdpack), ниже приводится простое решение для создания захватчика аргументов списка, который также отключает предупреждение «использует непроверенные или небезопасные операции» :

@SuppressWarnings("unchecked")
final ArgumentCaptor<List<SomeType>> someTypeListArgumentCaptor =
    ArgumentCaptor.forClass(List.class);

Полный пример здесь и соответствующий проход сборки CI и тестовый запуск здесь .

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

MRTS
источник
2

Для более ранней версии junit вы можете сделать

Class<Map<String, String>> mapClass = (Class) Map.class;
ArgumentCaptor<Map<String, String>> mapCaptor = ArgumentCaptor.forClass(mapClass);
quzhi65222714
источник
1

У меня была та же проблема с тестированием в моем приложении для Android. Я использовал ActivityInstrumentationTestCase2и MockitoAnnotations.initMocks(this);не работал. Я решил эту проблему с другим классом с соответствующим полем. Например:

class CaptorHolder {

        @Captor
        ArgumentCaptor<Callback<AuthResponse>> captor;

        public CaptorHolder() {
            MockitoAnnotations.initMocks(this);
        }
    }

Затем в методе проверки активности:

HubstaffService hubstaffService = mock(HubstaffService.class);
fragment.setHubstaffService(hubstaffService);

CaptorHolder captorHolder = new CaptorHolder();
ArgumentCaptor<Callback<AuthResponse>> captor = captorHolder.captor;

onView(withId(R.id.signInBtn))
        .perform(click());

verify(hubstaffService).authorize(anyString(), anyString(), captor.capture());
Callback<AuthResponse> callback = captor.getValue();
Тимофей Орищенко
источник
0

В GitHub Mockito есть открытая проблема именно об этой проблеме.

Я нашел простой обходной путь, который не заставляет вас использовать аннотации в ваших тестах:

import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.MockitoAnnotations;

public final class MockitoCaptorExtensions {

    public static <T> ArgumentCaptor<T> captorFor(final CaptorTypeReference<T> argumentTypeReference) {
        return new CaptorContainer<T>().captor;
    }

    public static <T> ArgumentCaptor<T> captorFor(final Class<T> argumentClass) {
        return ArgumentCaptor.forClass(argumentClass);
    }

    public interface CaptorTypeReference<T> {

        static <T> CaptorTypeReference<T> genericType() {
            return new CaptorTypeReference<T>() {
            };
        }

        default T nullOfGenericType() {
            return null;
        }

    }

    private static final class CaptorContainer<T> {

        @Captor
        private ArgumentCaptor<T> captor;

        private CaptorContainer() {
            MockitoAnnotations.initMocks(this);
        }

    }

}

То , что происходит здесь в том , что мы создаем новый класс с в @Captorаннотации и ввести в нее захватчик. Затем мы просто извлекаем захватчик и возвращаем его из нашего статического метода.

В своем тесте вы можете использовать его так:

ArgumentCaptor<Supplier<Set<List<Object>>>> fancyCaptor = captorFor(genericType());

Или с синтаксисом, который похож на Джексона TypeReference:

ArgumentCaptor<Supplier<Set<List<Object>>>> fancyCaptor = captorFor(
    new CaptorTypeReference<Supplier<Set<List<Object>>>>() {
    }
);

Это работает, потому что Mockito на самом деле не нуждается в информации о типах (в отличие от сериализаторов, например).

Jezor
источник