Como funcionam os fósforos do Mockito?

122

Matchers argumento Mockito (tais como any, argThat, eq, same, e ArgumentCaptor.capture()) se comportam de forma muito diferente a partir matchers hamcrest.

  • Os correspondências do Mockito freqüentemente causam InvalidUseOfMatchersException, mesmo no código que é executado muito tempo depois que qualquer correspondência foi usada.

  • Os matchers do Mockito estão sujeitos a regras estranhas, como exigir apenas o uso dos matchers do Mockito para todos os argumentos se um argumento em um determinado método usar um matcher.

  • Os matchers do Mockito podem causar NullPointerException ao substituir Answers ou ao usar (Integer) any()etc.

  • A refatoração de código com correspondências Mockito de certas maneiras pode produzir exceções e comportamento inesperado e pode falhar completamente.

Por que os fósforos Mockito são projetados assim e como eles são implementados?

Jeff Bowman
fonte

Respostas:

236

Os matchers do Mockito são métodos estáticos e chamadas para esses métodos, que substituem argumentos durante as chamadas para whene verify.

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

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

Existem correspondências Mockito, separadas das correspondências no estilo Hamcrest, para que as descrições das expressões correspondentes se ajustem diretamente às invocações de métodos : As correspondências Mockito retornam Tonde os métodos correspondentes do Hamcrest retornam objetos Matcher (do tipo Matcher<T>).

Matchers Mockito são invocadas através de métodos estáticos, como eq, any, gt, e startsWithno org.mockito.Matcherse org.mockito.AdditionalMatchers. Também existem adaptadores que foram alterados nas versões do Mockito:

  • Para o Mockito 1.x, Matchersalgumas chamadas em destaque (como intThatou argThat) são correspondências Mockito que aceitam diretamente correspondências Hamcrest como parâmetros. ArgumentMatcher<T>extended org.hamcrest.Matcher<T>, que foi usado na representação interna do Hamcrest e era uma classe base do combinador Hamcrest em vez de qualquer tipo de combinador Mockito.
  • Para o Mockito 2.0+, o Mockito não tem mais uma dependência direta do Hamcrest. Matcherschama formulado como intThatou argThatquebra de ArgumentMatcher<T>objetos que não são mais implementados, org.hamcrest.Matcher<T>mas são usados ​​de maneiras semelhantes. Adaptadores Hamcrest como argThate intThatainda estão disponíveis, mas foram movidos para MockitoHamcrestele.

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

/* 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 a Matcher<Integer>, que não funcionaria como setPowerLevelargumento. A correspondência Mockito intThatembrulha que Hamcrest-style Matcher e retorna um intmodo que pode aparecer como um argumento; Correspondentes de Mockito como agrupariam gt(9000)toda a expressão em uma única chamada, como na primeira linha do código de exemplo.

O que os jogadores fazem / retornam

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

Quando não estiver usando correspondentes de argumento, o Mockito registra seus valores de argumento e os compara com seus equalsmétodos.

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 anyou gt(maior que), o Mockito armazena um objeto correspondente que faz com que o Mockito pule essa verificação de igualdade e aplique sua correspondência de escolha. No caso, argumentCaptor.capture()ele armazena um correspondente que salva seu argumento para uma inspeção posterior.

Os correspondentes retornam valores fictícios como zero, coleções vazias ou null. Mockito tenta retornar um valor fictício seguro e apropriado, como 0 para anyInt()ou any(Integer.class)ou vazio List<String>para anyListOf(String.class). Por causa da exclusão de tipo, no entanto, o Mockito não possui informações de tipo para retornar qualquer valor, exceto nullpara any()or argThat(...), o que pode causar uma NullPointerException se tentar "descompactar automaticamente" um nullvalor primitivo.

Matchers gostam eqe gtaceitam valores de parâmetros; idealmente, esses valores devem ser calculados antes do início da verificação / remoção. Chamar uma zombaria no meio da zombaria de outra chamada pode interferir com o stub.

Os métodos correspondentes não podem ser usados ​​como valores de retorno; não há como expressar thenReturn(anyInt())ou thenReturn(any(Foo.class))no Mockito, por exemplo. O Mockito precisa saber exatamente qual instância retornar nas chamadas stubbing e não escolherá um valor de retorno arbitrário para você.

Detalhes da implementação

Os correspondentes são armazenados (como correspondentes do objeto no estilo Hamcrest) em uma pilha contida em uma classe chamada ArgumentMatcherStorage . MockitoCore e Matchers possuem uma instância ThreadSafeMockingProgress , que estaticamente contém um ThreadLocal que contém instâncias 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 segmento consistente entre as classes Mockito e Matchers.

A maioria das chamadas matcher só adicionar a esta pilha, com uma exceção para matchers como and, or, enot . Isso corresponde perfeitamente (e depende) da ordem de avaliação do Java , que avalia os argumentos da esquerda para a direita antes de chamar 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. Retirar gt(10)e lt(20)e adicionar and(gt(10), lt(20)).
  5. Chamada foo.quux(0, 0), que (a menos que seja stubada de outra forma) retorna o valor padrão false. Internamente, Mockito marca quux(int, int)como a chamada mais recente.
  6. Chamada when(false), que descarta seu argumento e se prepara para o método de stub quux(int, int)identificado em 5. Os únicos dois estados válidos são com tamanho de pilha 0 (igualdade) ou 2 (correspondências), e há duas correspondências na pilha (etapas 1 e 4), portanto Mockito stubs o método com um any()matcher para seu primeiro argumento e and(gt(10), lt(20))para o 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 quux(0, 0)com um int matcher na pilha. Conseqüentemente, se você usar um correspondente, precisará corresponder a todos os argumentos.

  • A ordem de chamada não é apenas importante, é o que faz tudo funcionar . Extrair matchers para variáveis ​​geralmente não funciona, porque geralmente altera a ordem das chamadas. Extrair matchers para métodos, 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 frequência suficiente para que Mockito não possa policiá-la com muito cuidado. Ele só pode verificar a pilha quando você interage com o Mockito ou com uma farsa, e precisa aceitar jogadores sem saber se eles são usados ​​imediatamente ou abandonados acidentalmente. Em teoria, a pilha sempre deve estar vazia fora de uma chamada para whenou 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 stubado o método para lançar uma exceção (ou exigir valores diferentes de zero ou não nulos). doReturne doAnswer(etc) não invocam o método real e geralmente são uma alternativa útil.

  • Se você tivesse chamado um método simulado no meio do stub (por exemplo, para calcular uma resposta para um eqmatcher), o Mockito verificaria o comprimento da pilha nessa chamada e provavelmente falharia.

  • Se você tentar fazer algo ruim, como stubbing / verificação de um método final , o Mockito chamará o método real e também deixará matchers extras na pilha . ofinal chamada do método pode não gerar uma exceção, mas você pode obter uma InvalidUseOfMatchersException dos correspondentes dispersos 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 você não usou uma correspondência fora de um whenverify chamada ou . Os correspondentes nunca devem ser usados ​​como valores de retorno stubbed ou campos / variáveis.

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

    • Verifique se você não está tentando stub / verificar um método final com um matcher. É uma ótima maneira de deixar um matcher na pilha e, a menos que seu método final gere uma exceção, talvez seja a única vez que você percebe que o método que está zombando é final.

  • NullPointerException com argumentos primitivos: (Integer) any() retorna nulo enquanto any(Integer.class)retorna 0; isso pode causar um NullPointerExceptionse você estiver esperando um em intvez de um número inteiro. De qualquer forma, prefira anyInt(), que retornará zero e também pulará a etapa de boxe automático.

  • NullPointerException ou outras exceções: as chamadas para when(foo.bar(any())).thenReturn(baz)realmente chamarão foo.bar(null) , as quais você pode ter stubbed para lançar uma exceção ao receber um argumento nulo. Mudar para doReturn(baz).when(foo).bar(any()) ignora o comportamento stubbed .

Solução de problemas gerais

  • Use MockitoJUnitRunner , ou chame explicitamente validateMockitoUsageem seu tearDownou@After método (o que o corredor faria por você automaticamente). Isso ajudará a determinar se você usou mal os matchers.

  • Para fins de depuração, adicione chamadas validateMockitoUsagediretamente ao seu código. Isso será lançado se você tiver algo na pilha, o que é um bom aviso de um sintoma ruim.

Jeff Bowman
fonte
2
Obrigado por este artigo. Uma NullPointerException com o formato when / thenReturn estava causando problemas, até que eu o alterei para doReturn / when.
Yngwietiger # 10/15
11

Apenas uma pequena adição à excelente resposta de Jeff Bowman, pois 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 whenchamada treinada de uma farsa , a ordem das whenchamadas é importante e deve ser da mais ampla à mais específica. A partir 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 when, o resultado será sempre true.

tibtof
fonte
2
Embora seja uma informação útil, trata-se de stubbing, não de matchers , portanto, pode não fazer sentido nessa questão. A ordem é importante, mas apenas porque a última cadeia de correspondência definida vence : isso significa que os stubs coexistentes geralmente são declarados mais específicos para o mínimo, mas, em alguns casos, você pode desejar uma substituição muito ampla do comportamento especificamente ridicularizado em um único caso de teste , momento em que uma definição ampla talvez precise vir por último.
Jeff Bowman
1
@JeffBowman Eu pensei que isso faz sentido nessa questão, já que a pergunta é sobre matchers mockito e os matchers podem ser usados ​​ao stubbing (como na maioria dos seus exemplos). Como pesquisar no google por uma explicação me levou a essa pergunta, acho útil ter essas informações aqui.
tibtof