Quando devo zombar?

138

Eu tenho um entendimento básico de objetos falsos e falsos, mas não tenho a certeza de ter um pressentimento sobre quando / onde usar a zombaria - especialmente como se aplicaria a esse cenário aqui .

Esteban Araya
fonte
Eu recomendo apenas zombar de dependências fora de processo e apenas delas, interações com as quais são observáveis ​​externamente (servidor SMTP, barramento de mensagens, etc.). Não zombe do banco de dados, é um detalhe de implementação. Mais sobre isso aqui: enterprisecraftsmanship.com/posts/when-to-mock
Vladimir

Respostas:

122

Um teste de unidade deve testar um único caminho de código por meio de um único método. Quando a execução de um método passa fora desse método, para outro objeto e volta novamente, você tem uma dependência.

Ao testar esse caminho de código com a dependência real, você não está testando a unidade; você está testando a integração. Embora isso seja bom e necessário, não é um teste de unidade.

Se sua dependência for incorreta, seu teste poderá ser afetado de forma a retornar um falso positivo. Por exemplo, você pode passar para a dependência um nulo inesperado e a dependência pode não ser lançada como nulo, conforme está documentado. Seu teste não encontra uma exceção de argumento nulo como deveria, e o teste passa.

Além disso, você pode achar difícil, se não impossível, fazer com que o objeto dependente retorne exatamente o que deseja durante um teste. Isso também inclui lançar exceções esperadas nos testes.

Um mock substitui essa dependência. Você define as expectativas nas chamadas para o objeto dependente, define os valores de retorno exatos que ele deve fornecer para executar o teste desejado e / ou quais exceções devem ser lançadas para que você possa testar seu código de manipulação de exceções. Dessa forma, você pode testar a unidade em questão facilmente.

TL; DR: zombe de todas as dependências que seu teste de unidade toca.

Drew Stephens
fonte
164
Essa resposta é muito radical. Os testes de unidade podem e devem exercer mais de um único método, desde que todos pertençam à mesma unidade coesa. Fazer o contrário exigiria muita zombaria / falsificação, levando a testes complicados e frágeis. Somente as dependências que realmente não pertencem à unidade em teste devem ser substituídas por zombaria.
Rogério
10
Essa resposta também é otimista demais. Seria melhor se incorporasse as falhas de objetos simulados de Jan.
Jeff Axelrod
1
Isso não é mais um argumento para injetar dependências para testes, em vez de zombar especificamente? Você poderia substituir "mock" por "stub" na sua resposta. Concordo que você deve zombar ou stub as dependências significativas. Eu já vi muitos códigos simulados que basicamente acabam reimplementando partes dos objetos simulados; zombarias certamente não são uma bala de prata.
Draemon
2
Zombe de todas as dependências que seu teste de unidade toca. Isso explica tudo.
precisa saber é o seguinte
2
TL; DR: zombe de todas as dependências que seu teste de unidade toca. - essa não é realmente uma ótima abordagem, diz o próprio mockito - não zombe de tudo. (downvoted)
p_champ
167

Objetos simulados são úteis quando você deseja testar interações entre uma classe em teste e uma interface específica.

Por exemplo, queremos testar esse método sendInvitations(MailServer mailServer)chama MailServer.createMessage()exatamente uma vez, e também chama MailServer.sendMessage(m)exatamente uma vez, e nenhum outro método é chamado na MailServerinterface. É quando podemos usar objetos simulados.

Com objetos simulados, em vez de passar por um real MailServerImplou por um teste TestMailServer, podemos passar por uma implementação simulada da MailServerinterface. Antes de passarmos por uma simulação MailServer, nós a "treinamos", para que ele saiba qual método chama esperar e quais valores retornam. No final, o objeto simulado afirma que todos os métodos esperados foram chamados conforme o esperado.

Isso parece bom em teoria, mas também existem algumas desvantagens.

Faltas simuladas

Se você possui uma estrutura simulada, é tentado a usar objeto simulado toda vez que precisar passar uma interface para a classe sob o teste. Dessa forma, você acaba testando interações mesmo quando não é necessário . Infelizmente, o teste indesejado (acidental) de interações é ruim, porque você está testando se um requisito específico é implementado de uma maneira específica, em vez de a implementação produzir o resultado necessário.

Aqui está um exemplo em pseudocódigo. Vamos supor que criamos uma MySorterclasse e queremos testá-la:

// the correct way of testing
testSort() {
    testList = [1, 7, 3, 8, 2] 
    MySorter.sort(testList)

    assert testList equals [1, 2, 3, 7, 8]
}


// incorrect, testing implementation
testSort() {
    testList = [1, 7, 3, 8, 2] 
    MySorter.sort(testList)

    assert that compare(1, 2) was called once 
    assert that compare(1, 3) was not called 
    assert that compare(2, 3) was called once 
    ....
}

(Neste exemplo, assumimos que não é um algoritmo de classificação específico, como a classificação rápida, que queremos testar; nesse caso, o último teste seria realmente válido.)

Em um exemplo tão extremo, é óbvio por que o último exemplo está errado. Quando alteramos a implementação de MySorter, o primeiro teste faz um ótimo trabalho para garantir que ainda classifiquemos corretamente, que é o objetivo dos testes - eles nos permitem alterar o código com segurança. Por outro lado, o último teste sempre interrompe e é ativamente prejudicial; dificulta a refatoração.

Zombarias como stubs

As estruturas simuladas geralmente permitem também um uso menos rigoroso, onde não precisamos especificar exatamente quantas vezes os métodos devem ser chamados e quais parâmetros são esperados; eles permitem criar objetos simulados que são usados ​​como stubs .

Vamos supor que temos um método sendInvitations(PdfFormatter pdfFormatter, MailServer mailServer)que queremos testar. O PdfFormatterobjeto pode ser usado para criar o convite. Aqui está o teste:

testInvitations() {
   // train as stub
   pdfFormatter = create mock of PdfFormatter
   let pdfFormatter.getCanvasWidth() returns 100
   let pdfFormatter.getCanvasHeight() returns 300
   let pdfFormatter.addText(x, y, text) returns true 
   let pdfFormatter.drawLine(line) does nothing

   // train as mock
   mailServer = create mock of MailServer
   expect mailServer.sendMail() called exactly once

   // do the test
   sendInvitations(pdfFormatter, mailServer)

   assert that all pdfFormatter expectations are met
   assert that all mailServer expectations are met
}

Neste exemplo, nós realmente não nos importamos com o PdfFormatterobjeto, apenas o treinamos para aceitar silenciosamente qualquer chamada e retornar alguns valores de retorno enlatados sensíveis para todos os métodos que sendInvitation()chamam neste momento. Como criamos exatamente essa lista de métodos para treinar? Simplesmente executamos o teste e continuamos adicionando os métodos até que o teste passasse. Observe que treinamos o esboço para responder a um método sem saber por que ele precisa chamá-lo. Simplesmente adicionamos tudo o que o teste reclamava. Estamos felizes, o teste passa.

Mas o que acontece depois, quando mudamos sendInvitations(), ou alguma outra classe que sendInvitations()usa, para criar pdfs mais sofisticados? Nosso teste falha repentinamente porque agora PdfFormattersão chamados mais métodos e não treinamos nosso esboço para esperá-los. E geralmente não é apenas um teste que falha em situações como essa, é qualquer teste que usa, direta ou indiretamente, o sendInvitations()método. Temos que corrigir todos esses testes adicionando mais treinamentos. Observe também que não podemos remover métodos que não são mais necessários, porque não sabemos quais deles não são necessários. Novamente, isso dificulta a refatoração.

Além disso, a legibilidade do teste sofreu terrivelmente, há muito código lá que não escrevemos por causa do que queríamos, mas porque precisávamos; não somos nós que queremos esse código lá. Testes que usam objetos simulados parecem muito complexos e geralmente são difíceis de ler. Os testes devem ajudar o leitor a entender como a classe sob o teste deve ser usada; portanto, devem ser simples e diretos. Se eles não são legíveis, ninguém os manterá; de fato, é mais fácil excluí-los do que mantê-los.

Como consertar isso? Facilmente:

  • Tente usar classes reais em vez de zombarias sempre que possível. Use o real PdfFormatterImpl. Se não for possível, altere as classes reais para torná-lo possível. Não poder usar uma classe em testes geralmente aponta para alguns problemas com a classe. Corrigir os problemas é uma situação em que todos saem ganhando - você corrigiu a turma e fez um teste mais simples. Por outro lado, não consertá-lo e usar zombarias é uma situação sem vitória - você não consertou a classe real e possui testes mais complexos e menos legíveis que impedem refatorações adicionais.
  • Tente criar uma implementação de teste simples da interface em vez de zombar dela em cada teste e use essa classe de teste em todos os seus testes. Crie TestPdfFormatterque não faz nada. Dessa forma, você pode alterá-lo uma vez para todos os testes e seus testes não são confusos com configurações longas em que você treina seus stubs.

Em suma, objetos simulados têm seu uso, mas quando não são usados ​​com cuidado, geralmente incentivam práticas ruins, testam detalhes de implementação, dificultam a refatoração e produzem testes difíceis de ler e difíceis de manter .

Para obter mais detalhes sobre deficiências de simulações, consulte também Objetos simulados: deficiências e casos de uso .

Jan Soltis
fonte
1
Uma resposta bem pensada, e eu concordo principalmente. Eu diria que, como os testes de unidade são de caixa branca, ter que alterá-los quando você altera a implementação para enviar PDFs mais sofisticados pode não ser um fardo irracional. Às vezes, as zombarias podem ser uma maneira útil de implementar rapidamente os stubs, em vez de ter muitas placas da caldeira. Na prática, parece que seu uso não é retratado nesses casos simples, no entanto.
Draemon
1
O ponto principal de uma simulação não é que seus testes são consistentes, que você não precisa se preocupar em zombar de objetos cujas implementações estão mudando continuamente, possivelmente por outros programadores sempre que você executa seu teste e obtém resultados consistentes.
PositiveGuy
1
Pontos muito bons e relevantes (especialmente sobre a fragilidade dos testes). Eu costumava usar mocks muito quando eu era mais jovem, mas agora eu considero teste de unidade que heavilly dependem de simulações como potencialmente disponível e se concentrar mais em testes de integração (com componentes reais)
Kemoda
6
"Não poder usar uma classe em testes geralmente indica alguns problemas com a classe." Se a classe é um serviço (por exemplo, acesso a banco de dados ou proxy para o serviço web), deve ser considerado como uma dependência externa e zombavam / apagou
Michael Freidgeim
1
Mas o que acontece depois, quando alteramos sendInvitations ()? Se o código em teste for modificado, ele não garante mais o contrato anterior e, portanto, deve falhar. E geralmente não é apenas um teste que falha em situações como essa . Se for esse o caso, o código não está implementado corretamente. A verificação de chamadas de método da dependência deve ser testada apenas uma vez (no teste de unidade apropriado). Todas as outras classes usarão apenas a instância simulada. Portanto, não vejo nenhum benefício misturando integração com testes de unidade.
Christopher Will
55

Regra prática:

Se a função que você está testando precisar de um objeto complicado como parâmetro, e seria difícil instanciar esse objeto (se, por exemplo, tentar estabelecer uma conexão TCP), use uma simulação.

Orion Edwards
fonte
4

Você deve zombar de um objeto quando tiver uma dependência em uma unidade de código que está tentando testar e que precisa ser "apenas isso".

Por exemplo, quando você está tentando testar alguma lógica em sua unidade de código, mas precisa obter algo de outro objeto e o que é retornado dessa dependência pode afetar o que você está tentando testar - zombe desse objeto.

Um ótimo podcast sobre o tema pode ser encontrado aqui

Toran Billups
fonte
O link agora é roteado para o episódio atual, não para o episódio pretendido. O podcast pretendido é este hanselminutes.com/32/mock-objects ?
C Perkins