Як захопити список конкретного типу за допомогою mockito


301

Чи є спосіб захоплення списку конкретного типу за допомогою mockitos ArgumentCaptore. Це не працює:

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

8
Я вважаю, що страшна ідея використовувати тут реалізацію конкретного списку ( ArrayList). Ви завжди можете використовувати Listінтерфейс, і якщо ви хочете представляти факт, що він коваріантний, ви можете використовувати extends:ArgumentCaptor<? extends List<SomeType>>
tenshi

Відповіді:


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

70
Я вважаю за краще використовувати MockitoAnnotations.initMocks(this)в @Beforeметоді , а не з допомогою бігунка , що виключає можливість використовувати іншу бігун. Однак +1, дякую за вказівку на примітку.
Джон Б

4
Не впевнений, що цей приклад завершено. Я отримую ... Помилка: (240, 40) java: змінний captor, можливо, не був ініціалізований. Мені подобається відповідь tenshi нижче
Michael Dausmann

1
Я зіткнувся з тією ж проблемою, і знайшов цей блог , який допоміг мені трохи: blog.jdriven.com/2012/10 / ... . Він включає в себе етап використання MockitoAnnotations.initMocks після того, як ви помістили анотацію до свого класу. Я помітив одне, що ви не можете мати його в локальній змінній.
SlopeOak

1
@ chamzz.dot ArgumentCaptor <ArrayList <SomeType>> captor; вже захоплює масив "SomeType" <- це специфічний тип, чи не так?
Мігель Р. Сантаелла

1
Я зазвичай віддаю перевагу списку замість ArrayList у декларації Captor: ArgumentCaptor <Список <SomeType>> captor;
Мігель Р. Сантаелла

146

Так, це загальна проблема генерики, не специфічна для макето.

Немає об’єкта класу для ArrayList<SomeType>, і, таким чином, ви не можете безпечно передати такий об'єкт методу, що вимагає 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>та an ArrayList<OtherType>.)

Редагувати:

Погляньте також на коментар tenshi . Ви можете змінити оригінальний код з Paŭlo Ebermann на цей (набагато простіше)

final ArgumentCaptor<List<SomeType>> listCaptor
        = ArgumentCaptor.forClass((Class) List.class);

49
Приклад, який ви показали, можна спростити, грунтуючись на тому, що java робить висновок типу для статичного виклику методу:ArgumentCaptor<List<SimeType>> argument = ArgumentCaptor.forClass((Class) List.class);
tenshi

4
Щоб вимкнути попередження про використання неперевірених чи небезпечних операцій , використовуйте @SuppressWarnings("unchecked")примітку над рядком визначення аргументу аргументу. Також кастинг на Classзайве.
мрц

1
У Classмоїх тестах кастинг не є зайвим.
Вім Деблауве

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.

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"));

4

На основі коментарів @ tenshi та @ pkalinow (також kudos to @rogerdpack), наступне просте рішення для створення перетворювача аргументів списку, який також вимикає попередження "використовує неперевірені або небезпечні операції" :

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

Повний приклад тут та відповідні проходження CI збірки та тестування тут .

Наша команда вже деякий час використовує це в наших тестах, і це виглядає як найпростіше рішення для нас.


2

Для більш ранньої версії junit ви можете зробити

Class<Map<String, String>> mapClass = (Class) Map.class;
ArgumentCaptor<Map<String, String>> mapCaptor = ArgumentCaptor.forClass(mapClass);

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 насправді не потребує будь-якої інформації (наприклад, на відміну від серіалізаторів).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.