Como os matchers do Mockito funcionam?

Os correspondentes de argumentos do Mockito (como any , argThat , eq , same e ArgumentCaptor.capture() ) se comportam de maneira muito diferente dos correspondentes do Hamcrest.

Por que os matchers do Mockito são projetados assim e como eles são implementados?

Correspondentes de Mockito são methods estáticos e chamadas para esses methods, que são argumentos durante as chamadas para when e verify .

Correspondentes de Hamcrest (versão arquivada) (ou correspondentes no estilo Hamcrest) são instâncias de objects de uso geral sem estado que implementam o Matcher e expõem um método que matches(T) retorna true se o object corresponder aos critérios do Matcher. Eles são destinados a ser livres de efeitos colaterais e são geralmente usados ​​em afirmações como a abaixo.

 /* Mockito */ verify(foo).setPowerLevel(gt(9000)); /* Hamcrest */ assertThat(foo.getPowerLevel(), is(greaterThan(9000))); 

Correspondentes de Mockito existem, separados dos correspondentes de estilo Hamcrest, para que as descrições de expressões correspondentes se encaixem diretamente nas chamadas de método : Correspondentes Mockito retornam T onde os methods de correspondência de Hamcrest retornam objects Correspondentes (do tipo Matcher ).

Os correspondentes do Mockito são invocados por meio de methods estáticos, como eq , any , gt e startsWith no org.mockito.Matchers e org.mockito.AdditionalMatchers . Existem também adaptadores que foram alterados nas versões do Mockito:

  • Para o Mockito 1.x, os Matchers apresentaram algumas chamadas (como intThat ou argThat ) que são correspondentes do Mockito que aceitam diretamente os matchers do Hamcrest como parâmetros. ArgumentMatcher estendido org.hamcrest.Matcher , que era usado na representação interna de Hamcrest e era uma class base de matcher de Hamcrest em vez de qualquer tipo de matcher de Mockito.
  • Para o Mockito 2.0+, o Mockito não tem mais dependência direta do Hamcrest. Matchers chama Matchers como intThat ou argThat intThat argThat ArgumentMatcher que não implementam mais org.hamcrest.Matcher mas são usados ​​de maneira semelhante. Adaptadores Hamcrest, como argThat e intThat ainda estão disponíveis, mas mudaram para o MockitoHamcrest .

Independentemente de os matchers serem do Hamcrest ou simplesmente do estilo Hamcrest, eles podem ser adaptados da seguinte maneira:

 /* Mockito matcher intThat adapting Hamcrest-style matcher is(greaterThan(...)) */ verify(foo).setPowerLevel(intThat(is(greaterThan(9000)))); 

Na declaração acima: foo.setPowerLevel é um método que aceita um int . is(greaterThan(9000)) retorna um Matcher , que não funcionaria como um argumento setPowerLevel . O matcher Mockito intThat aquele Matcher estilo Hamcrest e retorna um int para que possa aparecer como um argumento; Correspondentes de mockito como gt(9000) envolveriam toda a expressão em uma única chamada, como na primeira linha do código de exemplo.

Quais correspondências fazem / retornam

 when(foo.quux(3, 5)).thenReturn(true); 

Quando não está usando matchers de argumentos, Mockito registra seus valores de argumento e compara-os com seus methods de equals .

 when(foo.quux(eq(3), eq(5))).thenReturn(true); // same as above when(foo.quux(anyInt(), gt(5))).thenReturn(true); // this one's different 

Quando você chama um matcher como any ou gt (maior que), Mockito armazena um object matcher que faz com que Mockito pule a verificação de igualdade e aplique a sua combinação de escolha. No caso de argumentCaptor.capture() ele armazena um correspondente que salva seu argumento para inspeção posterior.

Correspondentes retornam valores fictícios , como zero, collections vazias ou null . Mockito tenta retornar um valor fictício seguro e apropriado, como 0 para anyInt() ou any(Integer.class) ou uma List anyListOf(String.class) para anyListOf(String.class) . Por causa do tipo apagar, no entanto, Mockito não possui informações de tipo para retornar qualquer valor, mas null para any() ou argThat(...) , que pode causar um NullPointerException se tentar “auto-unbox” um valor primitivo null .

Correspondentes como eq e gt obtêm valores de parâmetros; idealmente, esses valores devem ser calculados antes que o stub / verificação seja iniciado. Chamar uma simulação no meio de zombar de outra binding pode interferir no stub.

Métodos de correspondência não podem ser usados ​​como valores de retorno; não há maneira de frase thenReturn(anyInt()) ou thenReturn(any(Foo.class)) em Mockito, por exemplo. O Mockito precisa saber exatamente qual instância retornar em chamadas de stub e não escolherá um valor de retorno arbitrário para você.

Detalhes de implementação

Os correspondentes são armazenados (como correspondentes de objects no estilo Hamcrest) em uma pilha contida em uma class chamada ArgumentMatcherStorage . MockitoCore e Matchers possuem uma instância ThreadSafeMockingProgress , que contém estaticamente um ThreadLocal contendo instâncias de MockingProgress. É este MockingProgressImpl que contém um ArgumentMatcherStorageImpl concreto. Conseqüentemente, o estado de simulação e de correspondência é estático, mas com escopo de thread consistentemente entre as classs Mockito e Matchers.

A maioria das chamadas de matcher só é adicionada a essa pilha, com exceção de matchers like and , or , e not . Isso corresponde perfeitamente (e depende da) ordem de avaliação de Java , que avalia os argumentos da esquerda para a direita antes de invocar um método:

 when(foo.quux(anyInt(), and(gt(10), lt(20)))).thenReturn(true); [6] [5] [1] [4] [2] [3] 

Isso vai:

  1. Adicione anyInt() à pilha.
  2. Adicione gt(10) à pilha.
  3. Adicione lt(20) à pilha.
  4. Remover gt(10) e lt(20) e adicionar and(gt(10), lt(20)) .
  5. Chame foo.quux(0, 0) , o qual (a menos que seja stubbed) retorna o valor padrão false . Internamente, o Mockito marca o quux(int, int) como a chamada mais recente.
  6. Chame when(false) , que descarta seu argumento e se prepara para stub de método quux(int, int) identificado em 5. Os dois únicos estados válidos são com stack length 0 (equal) ou 2 (matchers), e há dois matchers em a pilha (passos 1 e 4), então Mockito stubs o método com um matcher any() para seu primeiro argumento e and(gt(10), lt(20)) para seu segundo argumento e limpa a pilha.

Isso demonstra algumas regras:

  • Mockito não pode dizer a diferença entre quux(anyInt(), 0) e quux(0, anyInt()) . Ambos se parecem com uma chamada para quux(0, 0) com um correspondente int na pilha. Conseqüentemente, se você usar um matcher, precisará corresponder a todos os argumentos.

  • A ordem das chamadas não é apenas importante, é o que faz tudo isso funcionar . Extrair matchers para variables ​​geralmente não funciona, porque geralmente altera a ordem das chamadas. Extrair matchers para methods, no entanto, funciona muito bem.

     int between10And20 = and(gt(10), lt(20)); /* BAD */ when(foo.quux(anyInt(), between10And20)).thenReturn(true); // Mockito sees the stack as the opposite: and(gt(10), lt(20)), anyInt(). public static int anyIntBetween10And20() { return and(gt(10), lt(20)); } /* OK */ when(foo.quux(anyInt(), anyIntBetween10And20())).thenReturn(true); // The helper method calls the matcher methods in the right order. 
  • A pilha muda com freqüência suficiente para que Mockito não possa policiá-lo com muito cuidado. Ele só pode verificar a pilha quando você interage com o Mockito ou um mock, e tem que aceitar matchers sem saber se eles são usados ​​imediatamente ou abandonados acidentalmente. Em teoria, a pilha deve estar sempre vazia fora de uma chamada para when ou verify , mas o Mockito não pode verificar isso automaticamente. Você pode verificar manualmente com Mockito.validateMockitoUsage() .

  • Em uma chamada para when , Mockito realmente chama o método em questão, que lançará uma exceção se você tiver stubbed o método para lançar uma exceção (ou exigir valores diferentes de zero ou não nulos). doReturn e doAnswer (etc) não invocam o método atual e geralmente são uma alternativa útil.

  • Se você tivesse chamado um método de simulação no meio de stub (por exemplo, para calcular uma resposta para um correspondente de eq ), Mockito verificaria o tamanho da pilha em relação a essa chamada e provavelmente falharia.

  • Se você tentar fazer algo ruim, como stubbing / verificar um método final , Mockito irá chamar o método real e também deixar matchers extras na pilha . A chamada final método não pode lançar uma exceção, mas você pode obter um InvalidUseOfMatchersException dos correspondentes de dispersão quando você interagir com uma simulação.

Problemas comuns

  • InvalidUseOfMatchersException :

    • Verifique se todos os argumentos têm exatamente uma chamada de correspondência, se você usa correspondências e se não usou um correspondente fora de uma chamada when ou verify . Os combinadores nunca devem ser usados ​​como valores de retorno ou campos / variables ​​stub.

    • Verifique se você não está chamando uma simulação como parte do fornecimento de um argumento de correspondência.

    • Verifique se você não está tentando resumir / verificar um método final com um correspondente. É uma ótima maneira de deixar um matcher na pilha, e a menos que seu método final lance uma exceção, essa pode ser a única vez que você percebe que o método que está mocking é final.

  • NullPointerException com argumentos primitivos: (Integer) any() retorna nulo enquanto any(Integer.class) retorna 0; Isso pode causar um NullPointerException se você estiver esperando um int vez de um Integer. Em qualquer caso, prefira anyInt() , que retornará zero e também ignorará a etapa de auto-boxing.

  • NullPointerException ou outras exceções: Chamadas para when(foo.bar(any())).thenReturn(baz) na verdade chamará foo.bar(null) , que você pode ter stubbed para lançar uma exceção ao receber um argumento nulo. Comutação para doReturn(baz).when(foo).bar(any()) ignora o comportamento stubbed .

Solução de problemas gerais

  • Use MockitoJUnitRunner ou chame explicitamente validateMockitoUsage em seu método @After ou @After (que o @After faria automaticamente por você). Isso ajudará a determinar se você usou mal ou não os correspondentes.

  • Para fins de debugging, adicione chamadas para validateMockitoUsage em seu código diretamente. Isso vai jogar se você tiver alguma coisa na pilha, que é um bom aviso de um sintoma ruim.

Apenas um pequeno acréscimo à excelente resposta de Jeff Bowman, quando encontrei essa pergunta ao procurar uma solução para um dos meus próprios problemas:

Se uma chamada para um método corresponder a mais de uma simulação when chamadas treinadas, a ordem das chamadas when for importante e deve ser da mais ampla para a mais específica. Começando de um dos exemplos de Jeff:

 when(foo.quux(anyInt(), anyInt())).thenReturn(true); when(foo.quux(anyInt(), eq(5))).thenReturn(false); 

é a ordem que garante o resultado (provavelmente) desejado:

 foo.quux(3 /*any int*/, 8 /*any other int than 5*/) //returns true foo.quux(2 /*any int*/, 5) //returns false 

Se você inverter as chamadas quando, o resultado será sempre true .