Algumas pessoas afirmam que os testes de integração são ruins e errados - tudo deve ser testado em unidade, o que significa que você precisa zombar de dependências; uma opção que, por várias razões, nem sempre gosto.
Acho que, em alguns casos, um teste de unidade simplesmente não prova nada.
Vamos tomar a seguinte implementação (trivial, ingênua) do repositório (em PHP) como exemplo:
class ProductRepository
{
private $db;
public function __construct(ConnectionInterface $db) {
$this->db = $db;
}
public function findByKeyword($keyword) {
// this might have a query builder, keyword processing, etc. - this is
// a totally naive example just to illustrate the DB dependency, mkay?
return $this->db->fetch("SELECT * FROM products p"
. " WHERE p.name LIKE :keyword", ['keyword' => $keyword]);
}
}
Digamos que eu queira provar em um teste que esse repositório pode realmente encontrar produtos que correspondem a várias palavras-chave.
Sem testes de integração com um objeto de conexão real, como posso saber que isso realmente está gerando consultas reais - e que essas consultas realmente fazem o que eu acho que elas fazem?
Se eu tiver que zombar do objeto de conexão em um teste de unidade, só posso provar coisas como "ele gera a consulta esperada" - mas isso não significa que ele realmente funcionará ... ou seja, talvez esteja gerando a consulta Eu esperava, mas talvez essa consulta não faça o que eu acho que faz.
Em outras palavras, sinto que um teste que faz afirmações sobre a consulta gerada é essencialmente sem valor, porque está testando como o findByKeyword()
método foi implementado , mas isso não prova que ele realmente funciona .
Esse problema não se limita aos repositórios ou à integração do banco de dados - parece aplicar-se em muitos casos, onde fazer afirmações sobre o uso de um mock (teste duplo) prova apenas como as coisas são implementadas, não se elas vão realmente funciona.
Como você lida com situações como essas?
Os testes de integração são realmente "ruins" em um caso como este?
Entendo que é melhor testar uma coisa e também entendo por que o teste de integração leva a uma infinidade de caminhos de código, os quais não podem ser testados - mas no caso de um serviço (como um repositório) cujo único objetivo é para interagir com outro componente, como você pode realmente testar alguma coisa sem o teste de integração?
fonte
Respostas:
Seu colega de trabalho está certo de que tudo o que pode ser testado em unidade deve ser testado em unidade, e você está certo de que os testes de unidade o levarão apenas até agora e não mais, principalmente ao escrever wrappers simples em serviços externos complexos.
Uma maneira comum de pensar sobre o teste é como uma pirâmide de teste . É um conceito frequentemente conectado ao Agile, e muitos já escreveram sobre ele, incluindo Martin Fowler (que o atribui a Mike Cohn em Sucesso com o Agile ), Alistair Scott e o Blog de testes do Google .
A noção é que testes de unidade resilientes e de execução rápida são a base do processo de teste - deve haver testes de unidade mais focados que testes de sistema / integração e mais testes de sistema / integração que testes de ponta a ponta. À medida que você se aproxima do topo, os testes tendem a levar mais tempo / recursos para serem executados, tendem a estar mais sujeitos a fragilidade e falhas e são menos específicos na identificação de qual sistema ou arquivo está quebrado ; naturalmente, é preferível evitar ser "pesado demais".
Nesse ponto, os testes de integração não são ruins , mas uma forte dependência deles pode indicar que você não projetou seus componentes individuais para serem fáceis de testar. Lembre-se, o objetivo aqui é testar o desempenho da sua unidade de acordo com as especificações, enquanto envolve um mínimo de outros sistemas que podem ser quebrados : você pode tentar um banco de dados em memória (que eu considero um teste fácil de testar duas vezes ao lado de zombarias) ) para testes pesados de casos extremos, por exemplo, e depois escreva alguns testes de integração com o mecanismo de banco de dados real para estabelecer que os casos principais funcionem quando o sistema é montado.
Como observação lateral, você mencionou que as simulações que você escreve simplesmente testam como algo é implementado, não se ele funciona . Isso é um antipadrão: um teste que é um espelho perfeito de sua implementação não está realmente testando nada. Em vez disso, teste se todas as classes ou métodos se comportam de acordo com suas próprias especificações , em qualquer nível de abstração ou realismo que exija.
fonte
É como dizer que os antibióticos são ruins - tudo deve ser curado com vitaminas.
Os testes de unidade não podem capturar tudo - eles apenas testam como um componente funciona em um ambiente controlado . Os testes de integração verificam que tudo funciona juntos , o que é mais difícil de fazer, mas mais significativo no final.
Um processo de teste bom e abrangente usa os dois tipos de testes - testes de unidade para verificar regras de negócios e outras coisas que podem ser testadas independentemente e testes de integração para garantir que tudo funcione em conjunto.
Você poderia testá-lo no nível do banco de dados . Execute a consulta com vários parâmetros e veja se obtém os resultados esperados. Concedido significa copiar / colar quaisquer alterações novamente no código "true". mas não permitem que você testar o independente consulta de quaisquer outras dependências.
fonte
Os testes de unidade não detectam todos os defeitos. Mas eles são mais baratos de configurar e (re) executam em comparação com outros tipos de testes. Os testes de unidade são justificados pela combinação de valor moderado e custo baixo a moderado.
Aqui está uma tabela mostrando as taxas de detecção de defeitos para diferentes tipos de teste.
fonte: p.470 em Code Complete 2 por McConnell
fonte
Não, eles não são ruins. Felizmente, é preciso ter testes de unidade e integração. Eles são usados e executados em diferentes estágios do ciclo de desenvolvimento.
Testes unitários
Os testes de unidade devem ser executados no servidor de compilação e localmente, após a compilação do código. Se algum teste de unidade falhar, deve-se falhar na compilação ou não confirmar a atualização do código até que os testes sejam corrigidos. A razão pela qual queremos testes de unidade isolados é que queremos que o servidor de compilação possa executar todos os testes sem todas as dependências. Em seguida, poderíamos executar a construção sem todas as dependências complexas necessárias e ter muitos testes que são executados muito rapidamente.
Portanto, para um banco de dados, deve-se ter algo como:
Agora, a implementação real do IRepository irá para o banco de dados para obter os produtos, mas para testes de unidade, pode-se zombar do IRepository com um falso para executar todos os testes conforme necessário, sem um banco de dados actaul, pois podemos simular todo tipo de lista de produtos sendo retornado da instância simulada e teste qualquer lógica de negócios com os dados simulados.
Testes de integração
Testes de integração geralmente são testes de cruzamento de limites. Queremos executar esses testes no servidor de implantação (o ambiente real), na caixa de proteção ou mesmo localmente (apontado para a caixa de proteção). Eles não são executados no servidor de construção. Depois que o software foi implantado no ambiente, normalmente eles seriam executados como atividade pós-implantação. Eles podem ser automatizados através de utilitários de linha de comando. Por exemplo, podemos executar o nUnit na linha de comando se categorizarmos todo o teste de integração que queremos chamar. Na verdade, eles chamam o repositório real com a chamada de banco de dados real. Esses tipos de testes ajudam com:
Às vezes, esses testes são mais difíceis de executar, pois também precisamos instalar e / ou desmontar. Considere adicionar um produto. Provavelmente, queremos adicionar o produto, consultá-lo para ver se ele foi adicionado e, depois que terminarmos, remova-o. Não queremos adicionar centenas ou milhares de produtos de "integração", portanto, é necessária uma configuração adicional.
Os testes de integração podem revelar-se bastante valiosos para validar um ambiente e garantir que a coisa real funcione.
Um deve ter os dois.
fonte
Testes de integração de banco de dados não são ruins. Ainda mais, eles são necessários.
Você provavelmente tem seu aplicativo dividido em camadas, e isso é uma coisa boa. Você pode testar cada camada isoladamente zombando das camadas vizinhas, e isso também é bom. Mas não importa quantas camadas de abstração você crie, em algum momento deve haver uma camada que faça o trabalho sujo - na verdade, converse com o banco de dados. A menos que você o teste, você não o faz . Se você testar a camada n zombando da camada n-1, estará avaliando a suposição de que a camada n funciona sob a condição de que a camada n-1 funcione. Para que isso funcione, você deve, de alguma forma, provar que a camada 0 funciona.
Embora em teoria você possa unidade de banco de dados de teste, analisando e interpretando o SQL gerado, é muito mais fácil e confiável criar banco de dados de teste rapidamente e conversar com ele.
Conclusão
Qual é a confiança obtida com os testes unitários das camadas Repositório Abstrato , Ethereal Objeto-Relacional-Mapeador , Registro Ativo Genérico e Persistência Teórica , quando no final o SQL gerado contém erro de sintaxe?
fonte
Você precisa dos dois.
No seu exemplo, se você estava testando um banco de dados em uma determinada condição, quando o
findByKeyword
método é executado, você obtém os dados de volta e espera que este seja um bom teste de integração.Em qualquer outro código que esteja usando esse
findByKeyword
método, você deseja controlar o que está sendo alimentado no teste, para que você possa retornar nulos ou as palavras certas para o seu teste ou o que quer que seja, para simular a dependência do banco de dados, para saber exatamente o que será o seu teste. receber (e você perde a sobrecarga de se conectar a um banco de dados e garantir que os dados contidos estejam corretos)fonte
O autor do artigo do blog ao qual você se refere se preocupa principalmente com a complexidade potencial que pode surgir dos testes integrados (embora seja escrito de uma maneira muito opinativa e categórica). No entanto, testes integrados não são necessariamente ruins e alguns são realmente mais úteis que testes de unidade puros. Realmente depende do contexto do seu aplicativo e do que você está tentando testar.
Atualmente, muitos aplicativos simplesmente não funcionariam se o servidor de banco de dados fosse desativado. Pelo menos, pense no contexto do recurso que você está tentando testar.
Por um lado, se o que você está tentando testar não depende, ou pode ser feito para não depender, do banco de dados, escreva seu teste de tal maneira que ele nem tente usar o banco de dados (basta fornecer dados simulados, conforme necessário). Por exemplo, se você está tentando testar alguma lógica de autenticação ao veicular uma página da web (por exemplo), provavelmente é uma boa coisa desanexá-la completamente do banco de dados (supondo que você não confie no banco de dados para autenticação ou que você pode zombar razoavelmente facilmente).
Por outro lado, se é um recurso que depende diretamente do seu banco de dados e que não funcionaria em um ambiente real, se o banco de dados não estivesse disponível, zombaria do que o banco de dados faz no seu código de cliente do banco de dados (ou seja, a camada que usa esse DB) não faz necessariamente sentido.
Por exemplo, se você sabe que seu aplicativo dependerá de um banco de dados (e possivelmente de um sistema de banco de dados específico), zombar do comportamento do banco de dados por causa disso geralmente será uma perda de tempo. Mecanismos de banco de dados (especialmente RDBMS) são sistemas complexos. Algumas linhas de SQL podem realmente executar muito trabalho, o que seria difícil de simular (na verdade, se sua consulta SQL tiver algumas linhas, é provável que você precise de muito mais linhas de Java / PHP / C # / Python código para produzir o mesmo resultado internamente): duplicar a lógica que você já implementou no banco de dados não faz sentido e verificar se o código de teste se tornaria um problema em si.
Eu não trataria isso necessariamente como um problema de teste de unidade versus teste integrado , mas sim olhar para o escopo do que está sendo testado. Os problemas gerais dos testes de unidade e integração permanecem: você precisa de um conjunto razoavelmente realista de dados e casos de teste, mas algo que também seja suficientemente pequeno para que os testes sejam executados rapidamente.
A hora de redefinir o banco de dados e preencher novamente com os dados de teste é um aspecto a considerar; você geralmente avaliaria isso contra o tempo que leva para escrever esse código falso (que você também precisaria manter também).
Outro ponto a considerar é o grau de dependência que seu aplicativo possui do banco de dados.
fonte
Você está certo ao pensar em um teste de unidade como incompleto. A incompletude está na interface do banco de dados sendo zombada. As expectativas ou afirmações ingênuas de tais simulações são incompletas.
Para completá-lo, você teria que poupar tempo e recursos suficientes para escrever ou integrar um mecanismo de regras SQL que garantisse que a instrução SQL emitida pelo sujeito em teste resultaria nas operações esperadas.
No entanto, a alternativa / companheira frequentemente esquecida e um tanto cara à zombaria é a "virtualização" .
Você pode gerar uma instância de banco de dados temporária, na memória, mas "real" para testar uma única função? sim ? lá, você tem um teste melhor, aquele que verifica os dados reais salvos e recuperados.
Agora, pode-se dizer, você transformou um teste de unidade em um teste de integração. Existem várias visualizações sobre onde desenhar a linha para classificar entre testes de unidade e testes de integração. IMHO, "unidade" é uma definição arbitrária e deve atender às suas necessidades.
fonte
Unit Tests
eIntegration Tests
são ortogonais entre si. Eles oferecem uma visão diferente sobre o aplicativo que você está construindo. Geralmente você quer os dois . Mas o momento é diferente, quando você deseja que tipo de teste.O mais frequentemente que você deseja
Unit Tests
. Os testes de unidade concentram-se em uma pequena parte do código que está sendo testado - o que exatamente é chamado deunit
é deixado para o leitor. Mas o objetivo é simples: obter feedback rápido de quando e onde seu código foi quebrado . Dito isto, deve ficar claro, que as chamadas para um banco de dados real não são válidas .Por outro lado, existem coisas que só podem ser testadas em condições difíceis sem um banco de dados. Talvez exista uma condição de corrida no seu código e uma chamada para um banco de dados lance uma violação de uma
unique constraint
que só poderia ser lançada se você realmente usar seu sistema. Mas esses tipos de testes são caros e você não pode (e não deseja) executá-los com a mesma frequênciaunit tests
.fonte
No mundo .Net, eu tenho o hábito de criar um projeto de teste e criar testes como um método de codificação / depuração / teste de ida e volta menos a interface do usuário. Esta é uma maneira eficiente de me desenvolver. Eu não estava tão interessado em executar todos os testes para cada build (porque diminui o fluxo do meu trabalho de desenvolvimento), mas entendo a utilidade disso para uma equipe maior. No entanto, você pode estabelecer que, antes de confirmar o código, todos os testes devem ser executados e aprovados (se levar mais tempo para os testes serem executados porque o banco de dados está realmente sendo atingido).
Zombar da camada de acesso a dados (DAO) e não atingir o banco de dados, não só não me permite codificar da maneira que eu gosto e me acostumei, mas perde uma grande parte da base de código real. Se você não está realmente testando a camada de acesso a dados e o banco de dados e apenas fingindo, e depois gastando muito tempo zombando das coisas, não consigo entender a utilidade dessa abordagem para realmente testar meu código. Estou testando um pedaço pequeno em vez de um pedaço maior com um teste. Entendo que minha abordagem pode ser mais parecida com um teste de integração, mas parece que o teste de unidade com a simulação é um desperdício de tempo redundante se você realmente escrever o teste de integração uma vez e primeiro. Também é uma boa maneira de desenvolver e depurar.
De fato, há algum tempo eu tenho conhecimento do TDD e do Behavior Driven Design (BDD) e pensando em maneiras de usá-lo, mas é difícil adicionar testes de unidade retroativamente. Talvez eu esteja errado, mas escrever um teste que cubra mais código de ponta a ponta com o banco de dados incluído, parece ser um teste de prioridade muito mais completo e com maior prioridade, que cobre mais código e é uma maneira mais eficiente de escrever testes.
Na verdade, acho que algo como o Behavior Driven Design (BDD), que tenta testar de ponta a ponta com a linguagem específica de domínio (DSL), deve ser o caminho a percorrer. Temos o SpecFlow no mundo .Net, mas ele começou como código aberto com o Pepino.
https://cucumber.io/
Eu realmente não estou impressionado com a verdadeira utilidade do teste que escrevi, zombando da camada de acesso a dados e não atingindo o banco de dados. O objeto retornado não atingiu o banco de dados e não foi preenchido com dados. Era um objeto inteiramente vazio que eu tinha que zombar de uma maneira não natural. Eu apenas acho que é uma perda de tempo.
De acordo com o Stack Overflow, a zombaria é usada quando objetos reais são impraticáveis para serem incorporados no teste de unidade.
https://stackoverflow.com/questions/2665812/what-is-mocking
"A zombaria é usada principalmente no teste de unidade. Um objeto em teste pode ter dependências de outros objetos (complexos). Para isolar o comportamento do objeto que você deseja testar, substitua os outros objetos por zombarias que simulam o comportamento dos objetos reais. Isso é útil se os objetos reais não forem práticos para incorporar no teste de unidade ".
Meu argumento é que, se eu estiver codificando algo de ponta a ponta (UI da web para camada comercial, camada de acesso a dados e banco de dados, ida e volta), antes de fazer o check-in como desenvolvedor, testarei esse fluxo de ida e volta. Se eu cortar a interface do usuário, depurar e testar esse fluxo a partir de um teste, testarei tudo que estiver fora da interface e retornarei exatamente o que a interface do usuário espera. Tudo o que me resta é enviar à interface do usuário o que ele deseja.
Eu tenho um teste mais completo que faz parte do meu fluxo de trabalho de desenvolvimento natural. Para mim, esse deve ser o teste de maior prioridade que cobre o teste das especificações reais do usuário, de ponta a ponta, o máximo possível. Se eu nunca criar outros testes mais detalhados, pelo menos eu tenho esse teste mais completo que prova que minha funcionalidade desejada funciona.
Um co-fundador do Stack Exchange não está convencido dos benefícios de ter 100% de cobertura de teste de unidade. Eu também não sou. Eu faria um "teste de integração" mais completo que atinge o banco de dados, mantendo vários zombarias de banco de dados a qualquer dia.
https://www.joelonsoftware.com/2009/01/31/from-podcast-38/
fonte
Dependências externas devem ser zombadas porque você não pode controlá-las (elas podem passar durante a fase de teste de integração, mas falham na produção). As unidades podem falhar, as conexões com o banco de dados podem falhar por várias razões, pode haver problemas de rede, etc. Fazer testes de integração não oferece confiança extra, pois são todos os problemas que podem ocorrer no tempo de execução.
Com testes de unidade verdadeiros, você está testando dentro dos limites da sandbox e deve ficar claro. Se um desenvolvedor escreveu uma consulta SQL que falhou no QA / PROD, isso significa que eles nem a testaram uma vez antes desse período.
fonte