Pergunta

Tenho um conhecimento básico de objetos simulados e falsos, mas não tenho certeza se tenho uma ideia sobre quando/onde usar a simulação - especialmente porque se aplicaria a este cenário aqui.

Foi útil?

Solução

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 vice-versa, 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 integração.Embora isso seja bom e necessário, não é um teste de unidade.

Se sua dependência apresentar erros, 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 nula como está documentado.Seu teste não encontra uma exceção de argumento nulo como deveria e o teste é aprovado.

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

Uma simulação substitui essa dependência.Você define expectativas nas chamadas para o objeto dependente, define os valores de retorno exatos que ele deve fornecer para realizar o teste desejado e/ou quais exceções lançar para que você possa testar seu código de tratamento de exceções.Desta forma você pode testar facilmente a unidade em questão.

DR:Zombe de cada dependência que seu teste de unidade toca.

Outras dicas

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) chamadas MailServer.createMessage() exatamente uma vez e também liga MailServer.sendMessage(m) exatamente uma vez, e nenhum outro método é chamado no MailServer interface.É quando podemos usar objetos simulados.

Com objetos simulados, em vez de passar um valor real MailServerImpl, ou um teste TestMailServer, podemos passar uma implementação simulada do MailServer interface.Antes de passarmos uma simulação MailServer, nós o "treinamos", para que ele saiba quais chamadas de método esperar e quais valores de retorno retornar.No final, o objeto simulado afirma que todos os métodos esperados foram chamados conforme esperado.

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

Deficiências simuladas

Se você tiver uma estrutura simulada em vigor, ficará tentado a usar um objeto simulado toda vez você precisa passar uma interface para a classe em teste.Dessa forma você acaba testar interações mesmo quando não é necessário.Infelizmente, testes indesejados (acidentais) de interações são ruins, porque então você está testando se um requisito específico foi implementado de uma maneira específica, em vez de se a implementação produziu o resultado necessário.

Aqui está um exemplo em pseudocódigo.Suponhamos que criamos um MySorter class 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.)

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

Zomba como esboços

Frameworks simulados geralmente também permitem um uso menos estrito, 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 tocos.

Suponhamos que temos um método sendInvitations(PdfFormatter pdfFormatter, MailServer mailServer) que queremos testar.O PdfFormatter objeto 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ão nos importamos realmente com o PdfFormatter objeto, então apenas o treinamos para aceitar silenciosamente qualquer chamada e retornar alguns valores de retorno predefinidos sensatos para todos os métodos que sendInvitation() acontece de ligar neste momento.Como criamos exatamente essa lista de métodos de treinamento?Simplesmente executamos o teste e continuamos adicionando os métodos até que o teste fosse aprovado.Observe que treinamos o stub para responder a um método sem ter ideia de por que ele precisa chamá-lo, simplesmente adicionamos tudo o que o teste reclamou.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 mais métodos de PdfFormatter são chamados e não treinamos nosso stub para esperá-los.E geralmente não é apenas um teste que falha em situações como essa, é qualquer teste que use, direta ou indiretamente, o sendInvitations() método.Temos que consertar 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 mais necessários.Novamente, isso dificulta a refatoração.

Além disso, a legibilidade do teste foi terrivelmente prejudicada, há muito código lá que não escrevemos porque queríamos, mas porque precisávamos;não somos nós que queremos esse código lá.Os 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 em teste deve ser utilizada, portanto, devem ser simples e diretos.Se não forem legíveis, ninguém irá mantê-los;na verdade, é mais fácil excluí-los do que mantê-los.

Como consertar isso?Facilmente:

  • Tente usar classes reais em vez de simulações sempre que possível.Use o verdadeiro PdfFormatterImpl.Caso não seja possível, altere as classes reais para que isso seja possível.Não poder usar uma classe em testes geralmente indica alguns problemas com a classe.Resolver os problemas é uma situação em que todos ganham - você consertou a classe e tem um teste mais simples.Por outro lado, não consertar e usar simulações é uma situação sem saída - você não corrigiu a classe real e tem testes mais complexos e menos legíveis que dificultam refatorações adicionais.
  • Tente criar uma implementação de teste simples da interface em vez de zombar dela em cada teste e use esta classe de teste em todos os seus testes.Criar TestPdfFormatter isso não faz nada.Dessa forma, você pode alterá-lo uma vez para todos os testes e seus testes não ficarão confusos com configurações demoradas onde você treina seus stubs.

Em suma, objetos simulados têm sua utilidade, mas quando não usados ​​com cuidado, eles muitas vezes incentivam práticas inadequadas, testando detalhes de implementação, dificultam a refatoração e produzem testes difíceis de ler e difíceis de manter.

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

Regra prática:

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

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 "exata".

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 - simule esse objeto.

Um ótimo podcast sobre o assunto pode ser encontrado aqui

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top