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 .
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 .
Respostas:
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.
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 MailServer
interface. É quando podemos usar objetos simulados.
Com objetos simulados, em vez de passar por um real MailServerImpl
ou por um teste TestMailServer
, podemos passar por uma implementação simulada da MailServer
interface. 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.
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 MySorter
classe 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.
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 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ós realmente não nos importamos com o PdfFormatter
objeto, 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 PdfFormatter
sã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:
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.TestPdfFormatter
que 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 .
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.
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