Comment capturer une liste de types spécifiques avec mockito

301

Existe-t-il un moyen de capturer une liste de types spécifiques à l'aide de mockitos ArgumentCaptore. Cela ne fonctionne pas:

ArgumentCaptor<ArrayList<SomeType>> argument = ArgumentCaptor.forClass(ArrayList.class);
Andreas Köberle
la source
8
Je trouve que c'est une idée terrible d'utiliser l'implémentation de liste concrète ici ( ArrayList). Vous pouvez toujours utiliser l' Listinterface, et si vous voulez représenter le fait que c'est covariant, alors vous pouvez utiliser extends:ArgumentCaptor<? extends List<SomeType>>
tenshi

Réponses:

533

Le problème des génériques imbriqués peut être évité avec l' annotation @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
la source
70
Je préfère utiliser MockitoAnnotations.initMocks(this)dans la @Beforeméthode plutôt que d'utiliser un coureur qui exclut la possibilité d'utiliser un autre coureur. Cependant, +1, merci d'avoir souligné l'annotation.
John B
4
Pas sûr que cet exemple soit complet. J'obtiens ... Erreur: (240, 40) java: le capteur de variables n'a peut-être pas été initialisé j'aime la réponse de tenshi ci
Michael Dausmann
1
J'ai rencontré le même problème et j'ai trouvé cet article de blog qui m'a un peu aidé: blog.jdriven.com/2012/10/… . Il comprend une étape pour utiliser MockitoAnnotations.initMocks après avoir mis l'annotation sur votre classe. Une chose que j'ai remarquée est que vous ne pouvez pas l'avoir dans une variable locale.
SlopeOak
1
@ chamzz.dot ArgumentCaptor <ArrayList <SomeType>> captor; capture déjà un tableau de "SomeType" <- c'est un type spécifique, n'est-ce pas?
Miguel R. Santaella
1
Je préfère généralement List au lieu de ArrayList dans la déclaration Captor: ArgumentCaptor <List <SomeType>> captor;
Miguel R. Santaella
146

Oui, c'est un problème générique général, pas spécifique aux mockito.

Il n'y a pas d'objet de classe pour ArrayList<SomeType>, et donc vous ne pouvez pas passer un tel objet en toute sécurité à une méthode nécessitant un Class<ArrayList<SomeType>>.

Vous pouvez convertir l'objet au bon type:

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

Cela donnera des avertissements sur les conversions dangereuses, et bien sûr, votre ArgumentCaptor ne peut pas vraiment faire la différence entre ArrayList<SomeType>et ArrayList<AnotherType>sans peut-être inspecter les éléments.

(Comme mentionné dans l'autre réponse, bien qu'il s'agisse d'un problème générique général, il existe une solution spécifique à Mockito pour le problème de sécurité de type avec l' @Captorannotation. Elle ne peut toujours pas distinguer entre un ArrayList<SomeType>et un ArrayList<OtherType>.)

Éditer:

Jetez également un œil au commentaire de tenshi . Vous pouvez changer le code original de Paŭlo Ebermann en ceci (beaucoup plus simple)

final ArgumentCaptor<List<SomeType>> listCaptor
        = ArgumentCaptor.forClass((Class) List.class);
Paŭlo Ebermann
la source
49
L'exemple que vous avez montré peut être simplifié, basé sur le fait que java fait l'inférence de type pour les appels de méthode statique:ArgumentCaptor<List<SimeType>> argument = ArgumentCaptor.forClass((Class) List.class);
tenshi
4
Pour désactiver l' avertissement des utilisations non contrôlées ou non sécurisées , utilisez l' @SuppressWarnings("unchecked")annotation au-dessus de la ligne de définition du capteur d'arguments. En outre, la diffusion vers Classest redondante.
mrts
1
Le casting Classn'est pas redondant dans mes tests.
Wim Deblauwe
16

Si vous n'avez pas peur de l'ancienne sémantique de style java (générique non sécurisé de type), cela fonctionne également et est assez 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
la source
2
Vous pouvez ajouter @SuppressWarnings ("rawtypes") avant la déclaration pour désactiver les avertissements.
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
la source
4

Sur la base des commentaires de @ tenshi et @ pkalinow (également bravo à @rogerdpack), ce qui suit est une solution simple pour créer un capteur d'argument de liste qui désactive également l' avertissement "utilise des opérations non contrôlées ou dangereuses" :

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

Exemple complet ici et génération de CI et test de passage correspondants ici .

Notre équipe l'utilise depuis un certain temps dans nos tests unitaires et cela semble être la solution la plus simple pour nous.

mrts
la source
2

Pour une version antérieure de junit, vous pouvez le faire

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

J'ai eu le même problème avec l'activité de test dans mon application Android. J'ai utilisé ActivityInstrumentationTestCase2et je MockitoAnnotations.initMocks(this);n'ai pas travaillé. J'ai résolu ce problème avec une autre classe avec respectivement champ. Par exemple:

class CaptorHolder {

        @Captor
        ArgumentCaptor<Callback<AuthResponse>> captor;

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

Ensuite, dans la méthode de test d'activité:

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
la source
0

Il y a un problème ouvert dans le GitHub de Mockito à propos de ce problème exact.

J'ai trouvé une solution de contournement simple qui ne vous oblige pas à utiliser des annotations dans vos tests:

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

    }

}

Ce qui se passe ici, c'est que nous créons une nouvelle classe avec l' @Captorannotation et y injectons le capteur. Ensuite, nous extrayons simplement le capteur et le renvoyons de notre méthode statique.

Dans votre test, vous pouvez l'utiliser comme ceci:

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

Ou avec une syntaxe qui ressemble à celle de Jackson TypeReference:

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

Cela fonctionne, car Mockito n'a en fait besoin d'aucune information de type (contrairement aux sérialiseurs, par exemple).

Jezor
la source