Quando usar o Mockito.verify ()?

201

Eu escrevo casos de teste jUnit para 3 propósitos:

  1. Para garantir que meu código satisfaça todas as funcionalidades necessárias, em todas (ou na maioria das) combinações / valores de entrada.
  2. Para garantir que eu possa alterar a implementação e confiar nos casos de teste JUnit para me informar que toda a minha funcionalidade ainda está satisfeita.
  3. Como documentação de todos os casos de uso que meu código lida e age como uma especificação para refatoração - caso o código precise ser reescrito. (Refatore o código e, se meus testes do jUnit falharem - você provavelmente perdeu algum caso de uso).

Não entendo por que ou quando Mockito.verify()deve ser usado. Quando vejo verify()ser chamado, está me dizendo que meu jUnit está se conscientizando da implementação. (Assim, alterar minha implementação quebraria minhas jUnits, mesmo que minha funcionalidade não fosse afetada).

Estou procurando por:

  1. Quais devem ser as diretrizes para o uso apropriado Mockito.verify()?

  2. É fundamentalmente correto que as jUnits estejam cientes ou fortemente acopladas à implementação da classe em teste?

Russell
fonte
1
Eu tento ficar longe de usar o check () o máximo que posso, pelo mesmo motivo que você expôs (não quero que meu teste de unidade tome conhecimento da implementação), mas existe um caso em que não tenho escolha - métodos vazios sem ponta. De um modo geral, como eles não retornam nada, eles não contribuem para sua saída 'real'; mas ainda assim, você precisa saber que foi chamado. Mas eu concordo com você, não faz sentido usar o verificar para verificar o fluxo de execução.
Legna

Respostas:

78

Se o contrato da classe A incluir o fato de chamar o método B de um objeto do tipo C, você deve testá-lo fazendo uma simulação do tipo C e verificando se o método B foi chamado.

Isso implica que o contrato da classe A possui detalhes suficientes para falar sobre o tipo C (que pode ser uma interface ou uma classe). Então, sim, estamos falando de um nível de especificação que vai além dos "requisitos do sistema" e descreve de alguma forma a implementação.

Isso é normal para testes de unidade. Quando você está testando a unidade, deseja garantir que cada unidade esteja fazendo a "coisa certa" e que geralmente inclui suas interações com outras unidades. "Unidades" aqui podem significar classes ou subconjuntos maiores do seu aplicativo.

Atualizar:

Eu sinto que isso não se aplica apenas à verificação, mas também ao stubbing. Assim que você stub um método de uma classe de colaborador, seu teste de unidade se torna, em certo sentido, dependente da implementação. É da natureza dos testes de unidade ser assim. Como o Mockito tem tanto a ver com stub quanto com verificação, o fato de você estar usando o Mockito implica que você encontrará esse tipo de dependência.

Na minha experiência, se eu alterar a implementação de uma classe, geralmente preciso alterar a implementação de seus testes de unidade para corresponder. Normalmente, porém, eu não vou ter que mudar o inventário do que testes de unidade não são para a classe; a não ser, é claro, o motivo da mudança foi a existência de uma condição que eu não testei anteriormente.

Então é disso que se trata os testes de unidade. Um teste que não sofre com esse tipo de dependência na maneira como as classes de colaborador são usadas é realmente um teste de subsistema ou de integração. Obviamente, eles também são frequentemente escritos com o JUnit e frequentemente envolvem o uso de zombaria. Na minha opinião, "JUnit" é um nome terrível, para um produto que nos permite produzir todos os tipos diferentes de teste.

Dawood ibn Kareem
fonte
8
Obrigado David. Após a varredura de alguns conjuntos de códigos, isso parece uma prática comum - mas, para mim, isso derrota o propósito de criar testes de unidade e apenas adiciona a sobrecarga de mantê-los por muito pouco valor. Entendo por que as simulações são necessárias e por que as dependências para executar o teste precisam ser configuradas. Mas verificar se o método dependencyA.XYZ () é executado torna os testes muito frágeis, na minha opinião.
Russell Russell
@ Russell Mesmo que o "tipo C" seja uma interface para um wrapper em torno de uma biblioteca ou em algum subsistema distinto do seu aplicativo?
Dawood ibn Karim
1
Eu não diria que é completamente inútil garantir que algum subsistema ou serviço seja chamado - apenas que haja algumas diretrizes em torno dele (formulá-las era o que eu queria fazer). Por exemplo: (provavelmente estou simplificando demais) Diga, estou usando StrUtil.equals () no meu código e decido mudar para StrUtil.equalsIgnoreCase () na implementação. Se a jUnit tiver verificado (StrUtil.equals ), meu teste pode falhar, embora a implementação seja precisa. Essa chamada de verificação, IMO, é uma prática ruim, embora seja para bibliotecas / subsistemas. Por outro lado, o uso de verificar para garantir que uma chamada para closeDbConn possa ser um caso de usuário válido.
Russell
1
Eu o entendo e concordo completamente com você. Mas também acho que escrever as diretrizes que você descreve pode se expandir para escrever um livro inteiro sobre TDD ou BDD. Para dar o seu exemplo, chamar equals()ou equalsIgnoreCase()nunca seria algo especificado nos requisitos de uma classe; portanto, nunca haveria um teste de unidade em si. No entanto, "fechar a conexão com o banco de dados quando concluído" (o que isso significa em termos de implementação) pode muito bem ser um requisito de uma classe, mesmo que não seja um "requisito de negócios". Para mim, isto resume-se à relação entre o contrato ...
Dawood ibn Kareem
... de uma classe, conforme expresso em seus requisitos de negócios, e o conjunto de métodos de teste que testam unitariamente essa classe. Definir esse relacionamento seria um tópico importante em qualquer livro sobre TDD ou BDD. Enquanto alguém da equipe do Mockito poderia escrever um post sobre esse tópico para o wiki, não vejo como isso seria diferente de muitas outras publicações disponíveis. Se você perceber como isso pode diferir, entre em contato e talvez possamos trabalhar juntos.
Dawood ibn Kareem
60

É claro que a resposta de David está correta, mas não explica exatamente por que você deseja isso.

Basicamente, ao testar a unidade, você está testando uma unidade de funcionalidade isoladamente. Você testa se a entrada produz a saída esperada. Às vezes, você também precisa testar os efeitos colaterais. Em poucas palavras, verificar permite que você faça isso.

Por exemplo, você tem um pouco de lógica comercial que deveria armazenar coisas usando um DAO. Você pode fazer isso usando um teste de integração que instancia o DAO, conecta-o à lógica de negócios e, em seguida, vasculha o banco de dados para verificar se o material esperado foi armazenado. Isso não é mais um teste de unidade.

Ou, você pode zombar do DAO e verificar se ele é chamado da maneira que você espera. Com o mockito, você pode verificar se algo é chamado, com que frequência é chamado e até usar correspondentes nos parâmetros para garantir que seja chamado de uma maneira específica.

O outro lado dos testes de unidade como esse é, de fato, que você está vinculando os testes à implementação, o que dificulta um pouco a refatoração. Por outro lado, um bom cheiro de design é a quantidade de código necessária para exercê-lo adequadamente. Se seus testes precisarem ser muito longos, provavelmente algo está errado com o design. Portanto, codificar com muitos efeitos colaterais / interações complexas que precisam ser testadas provavelmente não é uma coisa boa de se ter.

Jilles van Gurp
fonte
30

Esta é uma ótima pergunta! Acho que a causa raiz é a seguinte, estamos usando o JUnit não apenas para testes de unidade. Portanto, a questão deve ser dividida:

  • Devo usar o Mockito.verify () em meus testes de integração (ou qualquer outro teste superior à unidade)?
  • Devo usar Mockito.verify () no meu teste de unidade de caixa preta?
  • Devo usar o Mockito.verify () no meu teste de unidade de caixa branca ?

portanto, se ignorarmos o teste superior à unidade, a pergunta pode ser reformulada "O uso de teste de unidade de caixa branca com o Mockito.verify () cria um ótimo par entre o teste de unidade e a minha implementação, posso fazer " uma caixa cinza " teste de unidade e quais regras básicas eu devo usar para isso ".

Agora, vamos passar por tudo isso passo a passo.

* - Devo usar o Mockito.verify () em meus testes de integração (ou qualquer outro teste superior à unidade)? * Acho que a resposta é claramente não, além disso, você não deve usar zombarias para isso. Seu teste deve estar o mais próximo possível da aplicação real. Você está testando um caso de uso completo, não parte isolada do aplicativo.

* teste de unidade de caixa preta versus caixa branca * Se você estiver usando a abordagem de caixa preta o que realmente está fazendo, fornecerá (todas as classes de equivalência) entrada, um estado e testes para receber a saída esperada. Nesta abordagem, o uso de zombarias em geral é justificado (você apenas imita que eles estão fazendo a coisa certa; você não deseja testá-los), mas chamar Mockito.verify () é supérfluo.

Se você estiver usando a abordagem de caixa branca , o que realmente está fazendo, estará testando o comportamento da sua unidade. Nesta abordagem, chamar Mockito.verify () é essencial, você deve verificar se sua unidade se comporta conforme o esperado.

regras de ouro para o teste da caixa cinza O problema com o teste da caixa branca é que ele cria um alto acoplamento. Uma solução possível é fazer testes de caixa cinza, não de caixa branca. Esse é um tipo de combinação de testes em caixa preta e branca. Você está realmente testando o comportamento de sua unidade como nos testes de caixa branca, mas, em geral, torna-a independente de implementação, quando possível . Quando for possível, você fará uma verificação como no caso de caixa preta, apenas afirmará que a saída é o que se espera que seja. Portanto, a essência da sua pergunta é quando é possível.

Isso é realmente difícil. Não tenho um bom exemplo, mas posso dar exemplos. No caso mencionado acima com equals () vs equalsIgnoreCase (), você não deve chamar Mockito.verify (), apenas afirmar a saída. Se você não conseguiu, decomponha seu código na unidade menor, até conseguir. Por outro lado, suponha que você tenha algum @Service e esteja gravando o @ Web-Service que é essencialmente wrapper no seu @Service - ele delega todas as chamadas para o @Service (e faz um tratamento extra de erros). Nesse caso, chamar Mockito.verify () é essencial, você não deve duplicar todas as suas verificações feitas no @Serive, verificando se está ligando para o @Service com a lista correta de parâmetros.

alexsmail
fonte
O teste da caixa cinza é um pouco de armadilha. Costumo restringi-lo a coisas como DAOs. Eu estive em alguns projetos com compilações extremamente lentas por causa de uma abundância de testes de caixa cinza, uma falta quase completa de testes de unidade e muitos testes de caixa preta para compensar a falta de confiança no que os testes de caixa cinza estavam supostamente testando.
Jilles van Gurp
Para mim, essa é a melhor resposta disponível, pois ela responde quando usar o Mockito.when () em várias situações. Bem feito.
26319 Michiel Leegwater
8

Devo dizer que você está absolutamente certo do ponto de vista de uma abordagem clássica:

  • Se você primeiro criar (ou alterar) a lógica comercial do seu aplicativo e depois a cobrir com (adotar) testes ( abordagem Test-Last ), será muito doloroso e perigoso permitir que os testes saibam algo sobre como o seu software funciona, exceto verificação de entradas e saídas.
  • Se você estiver praticando uma abordagem orientada a testes, seus testes serão os primeiros a serem gravados, alterados e refletir os casos de uso da funcionalidade do seu software. A implementação depende de testes. Isso às vezes significa que você deseja que seu software seja implementado de alguma maneira específica, por exemplo, confie no método de algum outro componente ou até mesmo o chame uma quantidade específica de vezes. É aí que o Mockito.verify () é útil!

É importante lembrar que não existem ferramentas universais. O tipo de software, tamanho, objetivos da empresa e situação do mercado, habilidades da equipe e muitas outras coisas influenciam a decisão sobre qual abordagem usar no seu caso específico.

hammelion
fonte
0

Como algumas pessoas disseram

  1. Às vezes, você não tem uma saída direta na qual possa afirmar
  2. Às vezes, você só precisa confirmar que seu método testado está enviando as saídas indiretas corretas para seus colaboradores (que você está zombando).

Com relação à sua preocupação em interromper seus testes ao refatorar, isso é algo esperado ao usar zombarias / stubs / spies. Quero dizer que por definição e não em relação a uma implementação específica como o Mockito. Mas você pode pensar dessa maneira - se você precisar fazer uma refatoração que crie grandes mudanças na maneira como seu método funciona, é uma boa idéia fazê-lo em uma abordagem TDD, o que significa que você pode alterar seu teste primeiro para definir o novo comportamento (que irá falhar o teste), e , em seguida, fazer as alterações e obter o teste passou novamente.

Emanuel Luiz Lariguet Beltrame
fonte
0

Na maioria dos casos, quando as pessoas não gostam de usar o Mockito.verify, é porque é usado para verificar tudo o que a unidade testada está fazendo e isso significa que você precisará adaptar seu teste se houver alguma alteração nela. Mas não acho que isso seja um problema. Se você deseja alterar o que um método faz sem a necessidade de alterar seu teste, isso significa basicamente que você deseja escrever testes que não testam tudo o que seu método está fazendo, porque você não deseja que ele teste suas alterações . E essa é a maneira errada de pensar.

O que realmente é um problema é se você pode modificar o que o seu método faz e um teste de unidade que deve cobrir totalmente a funcionalidade não falha. Isso significa que, qualquer que seja a intenção da sua alteração, o resultado da sua alteração não será coberto pelo teste.

Por isso, prefiro zombar o máximo possível: zombe também de seus objetos de dados. Ao fazer isso, você pode usar não só verificar para verificar se os métodos corretos de outras classes são chamados, mas também que os dados passados ​​são coletados pelos métodos corretos desses objetos de dados. E para completar, você deve testar a ordem em que as chamadas ocorrem. Exemplo: se você modificar um objeto de entidade db e, em seguida, salvá-lo usando um repositório, não será suficiente verificar se os setters do objeto são chamados com os dados corretos e se o método save do repositório é chamado. Se eles forem chamados na ordem errada, seu método ainda não fará o que deveria. Portanto, não uso o Mockito.verify, mas crio um objeto inOrder com todas as zombarias e, em vez disso, uso o inOrder.verify. E se você quiser completá-lo, também deve ligar para Mockito. Verifique NoMoreInteractions no final e passe todas as zombarias. Caso contrário, alguém poderá adicionar nova funcionalidade / comportamento sem testá-lo, o que significaria depois que suas estatísticas de cobertura puderem ser 100% e você ainda estará acumulando código que não é afirmado ou verificado.

Stefan Mondelaers
fonte