Como capturar uma lista de tipos específicos com mockito

301

Existe uma maneira de capturar uma lista de tipos específicos usando mockitos ArgumentCaptore. Isso não funciona:

ArgumentCaptor<ArrayList<SomeType>> argument = ArgumentCaptor.forClass(ArrayList.class);
Andreas Köberle
fonte
8
Acho que é uma péssima idéia usar a implementação de lista concreta aqui ( ArrayList). Você sempre pode usar Listinterface, e se você quer representar o fato, de que é co-variante, então você pode usar extends:ArgumentCaptor<? extends List<SomeType>>
tenshi

Respostas:

533

O problema genérico aninhado pode ser evitado com a anotação @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
fonte
70
Prefiro usar MockitoAnnotations.initMocks(this)o @Beforemétodo do que usar um corredor que exclua a capacidade de usar outro corredor. No entanto, +1, obrigado por apontar a anotação.
John B
4
Não tenho certeza se este exemplo está completo. Eu recebo ... Erro: (240, 40) java: o capttor variável pode não ter sido inicializado eu gosto da resposta de tenshi abaixo #
505 Michael Dausmann
1
Encontrei o mesmo problema e encontrei este post que me ajudou um pouco: blog.jdriven.com/2012/10/… . Inclui uma etapa para usar o MockitoAnnotations.initMocks depois de colocar a anotação em sua classe. Uma coisa que notei é que você não pode tê-lo em uma variável local.
SlopeOak
1
@ chamzz.dot ArgumentCaptor <ArrayList <SomeType>> captor; já está capturando uma matriz de "SomeType" <- que é um tipo específico, não é?
Miguel R. Santaella
1
Eu geralmente prefiro List em vez de ArrayList na declaração Captor: ArgumentCaptor <List <SomeType>> captor;
Miguel R. Santaella
146

Sim, esse é um problema genérico geral, não específico de mockito.

Não existe um objeto de classe ArrayList<SomeType>e, portanto, você não pode digitar com segurança esse objeto para um método que exija a Class<ArrayList<SomeType>>.

Você pode converter o objeto no tipo certo:

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

Isso fornecerá alguns avisos sobre lançamentos inseguros e, é claro, seu ArgumentCaptor não pode realmente diferenciar entre ArrayList<SomeType>e ArrayList<AnotherType>sem talvez inspecionar os elementos.

(Conforme mencionado na outra resposta, embora esse seja um problema genérico geral, existe uma solução específica do Mockito para o problema de segurança de tipo na @Captoranotação. Ainda não é possível distinguir entre um ArrayList<SomeType>e um ArrayList<OtherType>.)

Editar:

Veja também o comentário de tenshi . Você pode alterar o código original de Paŭlo Ebermann para este (muito mais simples)

final ArgumentCaptor<List<SomeType>> listCaptor
        = ArgumentCaptor.forClass((Class) List.class);
Paŭlo Ebermann
fonte
49
O exemplo que você mostrou pode ser simplificado, com base no fato de que o java faz inferência de tipo para as chamadas de método estático:ArgumentCaptor<List<SimeType>> argument = ArgumentCaptor.forClass((Class) List.class);
tenshi
4
Para desativar o aviso de operações desmarcadas ou não seguras , use a @SuppressWarnings("unchecked")anotação acima da linha de definição do captador de argumento. Além disso, transmitir para Classé redundante.
Mrts
1
A transmissão para Classnão é redundante nos meus testes.
Wim Deblauwe 26/04/19
16

Se você não tem medo da semântica antiga do estilo java (não genérico seguro), isso também funciona e é razoavelmente simples.

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
fonte
2
Você pode adicionar @SuppressWarnings ("rawtypes") antes da declaração para desativar os avisos.
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
fonte
4

Com base nos comentários de @ tenshi e @ pkalinow (também parabéns a @rogerdpack), a seguir é uma solução simples para criar um captador de argumentos de lista que também desativa o aviso "usa operações não verificadas ou não seguras" :

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

Exemplo completo aqui e construção e teste de ICs correspondentes correspondentes executados aqui .

Nossa equipe vem usando isso há algum tempo em nossos testes de unidade e essa parece a solução mais direta para nós.

Mrts
fonte
2

Para uma versão anterior do junit, você pode fazer

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

Eu tive o mesmo problema com a atividade de teste no meu aplicativo Android. Eu usei ActivityInstrumentationTestCase2e MockitoAnnotations.initMocks(this);não funcionei. Eu resolvi esse problema com outra classe com o campo respectivamente. Por exemplo:

class CaptorHolder {

        @Captor
        ArgumentCaptor<Callback<AuthResponse>> captor;

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

Em seguida, no método de teste de atividade:

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
fonte
0

Há um problema em aberto no GitHub do Mockito sobre esse problema exato.

Eu encontrei uma solução simples que não o força a usar anotações em seus testes:

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

    }

}

O que acontece aqui é que criamos uma nova classe com a @Captoranotação e injetamos o captador nela. Depois, extraímos o captador e o devolvemos do nosso método estático.

No seu teste, você pode usá-lo da seguinte maneira:

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

Ou com uma sintaxe semelhante à de Jackson TypeReference:

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

Funciona porque o Mockito não precisa de nenhuma informação de tipo (ao contrário dos serializadores, por exemplo).

Jezor
fonte