Je rencontre un problème de génériques avec Mockito et Hamcrest.
S'il vous plaît assumer l'interface suivante:
public interface Service {
void perform(Collection<String> elements);
}
Et l'extrait de test suivant:
Service service = mock(Service.class);
// ... perform business logic
verify(service).perform(Matchers.argThat(contains("a", "b")));
Je souhaite donc vérifier que ma logique métier a bien appelé le service avec une collection contenant "a" et "b" dans cet ordre.
Cependant, le type de retour de contains(...)
est Matcher<Iterable<? extends E>>
, donc Matchers.argThat(...)
renvoie Iterable<String>
dans mon cas, ce qui naturellement ne s'applique pas au Collection<String>
requis.
Je sais que je pourrais utiliser un capteur d’arguments tel que proposé dans Hamcrest hasItem et Mockito vérifient l’incohérence , mais j’aimerais bien ne pas le faire.
Toutes les suggestions! Merci!
Vous pouvez simplement écrire
verify(service).perform((Collection<String>) Matchers.argThat(contains("a", "b")));
Du point de vue du compilateur, cela transforme un Iterable<String>
en un Collection<String>
, ce qui est correct, car ce dernier est un sous-type du premier. Au moment de l'exécution, argThat
renverra null
, de sorte qu'il peut être passé à perform
sans ClassCastException
. Le point important à ce sujet est que le correcteur accède à la structure interne des arguments de vérification de Mockito, comme le fait argThat
.
Si vous vous retrouvez dans de telles situations, n’oubliez pas que vous pouvez écrire un très petit adaptateur réutilisable.
verify(service).perform(argThat(isACollectionThat(contains("foo", "bar"))));
private static <T> Matcher<Collection<T>> isACollectionThat(
final Matcher<Iterable<? extends T>> matcher) {
return new BaseMatcher<Collection<T>>() {
@Override public boolean matches(Object item) {
return matcher.matches(item);
}
@Override public void describeTo(Description description) {
matcher.describeTo(description);
}
};
}
Notez que la solution de David ci-dessus, avec le casting, est la réponse la plus courte et correcte.
Comme alternative, on pourrait changer l'approche en ArgumentCaptor
:
@SuppressWarnings("unchecked") // needed because of `List<String>.class` is not a thing
// suppression can be worked around by using @Captor on a field
ArgumentCaptor<List<String>> captor = ArgumentCaptor.forClass(List.class);
verify(service).perform(captor.capture());
assertThat(captor.getValue(), contains("a", "b"));
Pourquoi ne pas simplement vérifier avec les arguments attendus, en supposant que la liste ne contienne que les deux éléments, par exemple:
final List<String> expected = Lists.newArrayList("a", "b");
verify(service).perform(expected);
Bien que je sois d’accord avec le principe d’Eugen, je pense que s’appuyer sur des équivalents pour la comparaison de chaînes est acceptable… De plus, le matriciel contains
utilise des égaux pour la comparaison.
Vous pouvez mettre votre propre lambda en tant que ArgumentMatcher
when(myClass.myMethod(argThat(arg -> arg.containsAll(asList(1,2))))
.thenReturn(...);
Vous pouvez avoir votre propre implémentation Java.util.Collection et écraser la méthode equals comme ci-dessous.
public interface Service {
void perform(Collection<String> elements);
}
@Test
public void testName() throws Exception {
Service service = mock(Service.class);
service.perform(new HashSet<String>(Arrays.asList("a","b")));
Mockito.verify(service).perform(Matchers.eq(new CollectionVerifier<String>(Arrays.asList("a","b"))));
}
public class CollectionVerifier<E> extends ArrayList<E> {
public CollectionVerifier() {
}
public CollectionVerifier(final Collection<? extends E> c) {
super(c);
}
@Override
public boolean equals(final Object o) {
if (o instanceof Collection<?>) {
Collection<?> other = (Collection<?>) o;
return this.size() == other.size() && this.containsAll(other);
}
return false;
}
}