Cómo capturar una lista de tipos específicos con mockito

301

¿Hay alguna manera de capturar una lista de tipos específicos usando mockitos ArgumentCaptore? Esto no funciona:

ArgumentCaptor<ArrayList<SomeType>> argument = ArgumentCaptor.forClass(ArrayList.class);
Andreas Köberle
fuente
8
Encuentro que es una idea terrible usar una implementación de lista concreta aquí ( ArrayList). Siempre puedes usar la Listinterfaz, y si quieres representar el hecho de que es covariante, entonces puedes usar extends:ArgumentCaptor<? extends List<SomeType>>
tenshi

Respuestas:

533

El problema genérico anidado se puede evitar con la anotación @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
fuente
70
Prefiero usar MockitoAnnotations.initMocks(this)el @Beforemétodo en lugar de usar un corredor que excluye la posibilidad de usar otro corredor. Sin embargo, +1, gracias por señalar la anotación.
John B
44
No estoy seguro de que este ejemplo esté completo. Me sale ... Error: (240, 40) java: el captor variable podría no haberse inicializado, me gusta la respuesta de tenshi a continuación
Michael Dausmann
1
Me encontré con el mismo problema y encontré esta publicación de blog que me ayudó un poco: blog.jdriven.com/2012/10/… . Incluye un paso para usar MockitoAnnotations.initMocks después de poner la anotación en su clase. Una cosa que noté es que no se puede tener dentro de una variable local.
SlopeOak
1
@ chamzz.dot ArgumentCaptor <ArrayList <SomeType>> captor; ya está capturando una matriz de "SomeType" <, que es un tipo específico, ¿no?
Miguel R. Santaella
1
Normalmente prefiero List en lugar de ArrayList en la declaración de Captor: ArgumentCaptor <List <SomeType>> captor;
Miguel R. Santaella
146

Sí, este es un problema genérico general, no específico de simulacro.

No hay ningún objeto de clase ArrayList<SomeType>y, por lo tanto, no puede escribir con seguridad dicho objeto a un método que requiera un Class<ArrayList<SomeType>>.

Puede lanzar el objeto al tipo correcto:

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

Esto le dará algunas advertencias sobre los lanzamientos inseguros y, por supuesto, su ArgumentCaptor no puede realmente diferenciar entre ArrayList<SomeType>y ArrayList<AnotherType>sin quizás inspeccionar los elementos.

(Como se mencionó en la otra respuesta, si bien este es un problema genérico general, existe una solución específica de Mockito para el problema de seguridad de tipo con la @Captoranotación. Todavía no se puede distinguir entre an ArrayList<SomeType>y an ArrayList<OtherType>.)

Editar:

Mire también el comentario de tenshi . Puede cambiar el código original de Paŭlo Ebermann a esto (mucho más simple)

final ArgumentCaptor<List<SomeType>> listCaptor
        = ArgumentCaptor.forClass((Class) List.class);
Paŭlo Ebermann
fuente
49
El ejemplo que mostró puede simplificarse, basado en el hecho de que Java hace inferencia de tipo para las llamadas al método estático:ArgumentCaptor<List<SimeType>> argument = ArgumentCaptor.forClass((Class) List.class);
tenshi
44
Para deshabilitar las advertencias de operaciones no comprobadas o inseguras , use la @SuppressWarnings("unchecked")anotación sobre la línea de definición del captor de argumentos. Además, enviar a Classes redundante.
mrts
1
El envío a Classno es redundante en mis pruebas.
Wim Deblauwe
16

Si no le temes a la antigua semántica de estilo java (genérico sin tipo seguro), esto también funciona y es razonablemente simple:

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
fuente
2
Puede agregar @SuppressWarnings ("rawtypes") antes de la declaración para deshabilitar las advertencias.
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
fuente
4

Basado en los comentarios de @ tenshi y @ pkalinow (también felicitaciones a @rogerdpack), la siguiente es una solución simple para crear un captor de argumentos de lista que también deshabilita la advertencia "utiliza operaciones no verificadas o inseguras" :

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

Ejemplo completo aquí y la compilación y prueba de CI correspondientes que se ejecutan aquí .

Nuestro equipo ha estado usando esto durante algún tiempo en nuestras pruebas unitarias y esta parece ser la solución más sencilla para nosotros.

mrts
fuente
2

Para una versión anterior de junit, puedes hacer

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

Tuve el mismo problema con la actividad de prueba en mi aplicación de Android. Solía ActivityInstrumentationTestCase2y MockitoAnnotations.initMocks(this);no funcionaba. Resolví este problema con otra clase con campo respectivamente. Por ejemplo:

class CaptorHolder {

        @Captor
        ArgumentCaptor<Callback<AuthResponse>> captor;

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

Luego, en el método de prueba de actividad:

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();
Timofey Orischenko
fuente
0

Hay un problema abierto en GitHub de Mockito sobre este problema exacto.

He encontrado una solución simple que no te obliga a usar anotaciones en tus pruebas:

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

    }

}

Lo que sucede aquí es que creamos una nueva clase con la @Captoranotación e inyectamos el captor en ella. Luego simplemente extraemos el captor y lo devolvemos de nuestro método estático.

En su prueba, puede usarlo así:

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

O con una sintaxis que se parece a la de Jackson TypeReference:

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

Funciona porque Mockito no necesita ningún tipo de información (a diferencia de los serializadores, por ejemplo).

Jezor
fuente