Os testes de integração (banco de dados) estão incorretos?

116

Algumas pessoas mantêm que os testes de integração são todos os tipos de erros e erros - tudo deve ser testado em unidade, o que significa que você precisa simular dependências; uma opção que, por várias razões, nem sempre gosto de fazer.

Eu acho que, em alguns casos, um teste de unidade simplesmente não prova nada.

Vamos usar a seguinte implementação (trivial e 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 encontrar produtos correspondentes a várias palavras-chave.

Falta de testes de integração com um objeto de conexão real, como posso saber que isso está realmente gerando consultas reais - e que essas consultas realmente fazem o que eu acho que 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 vá funcionar . .. isto é, talvez esteja gerando a consulta que 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 método findByKeyword() foi implementado , mas isso não prova que realmente funciona .

Este problema não está limitado a repositórios ou integração de banco de dados - parece aplicar-se em muitos casos, onde fazer afirmações sobre o uso de um mock (teste duplo) apenas prova como as coisas são implementadas, não se elas vamos realmente trabalhar.

Como você lida com situações como essas?

Os testes de integração são realmente "ruins" em um caso como esse?

Eu entendo que é melhor testar uma coisa e também entendo por que o teste de integração leva a uma miríade de caminhos de código, os quais não podem ser testados - mas no caso de um serviço (como um repositório) O único propósito é interagir com outro componente, como você pode realmente testar qualquer coisa sem testes de integração?

    
por mindplay.dk 02.11.2015 / 17:58
fonte

11 respostas

123

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 que os testes unitários levarão você apenas até agora e não mais, particularmente ao escrever invólucros simples em serviços externos complexos. / p>

Uma maneira comum de pensar sobre o teste é como uma pirâmide de testes . É um conceito freqüentemente conectado com o Agile, e muitos escreveram sobre ele, incluindo Martin Fowler (que atribui isso a Mike Cohn em Sucedendo com Agile ), Alistair Scott , e o Google Blog de testes .

        /\                           --------------
       /  \        UI / End-to-End    \          /
      /----\                           \--------/
     /      \     Integration/System    \      /
    /--------\                           \----/
   /          \          Unit             \  /
  --------------                           \/
  Pyramid (good)                   Ice cream cone (bad)

A ideia é que os testes de unidade resilientes e de execução rápida são a base do processo de teste - deve haver testes unitários mais focados do que testes de sistema / integração e mais testes de integração / sistema do 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 sujeitos a mais fragilidade e descamação e são menos específicos na identificação de qual sistema ou arquivo está quebrado ; naturalmente, é preferível evitar ser "top pesado".

Até esse ponto, os testes de integração não são ruins , mas a 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 se a sua unidade está funcionando de acordo com sua especificação, envolvendo um mínimo de outros sistemas quebráveis : Você pode tentar um banco de dados na memória (que eu conto como uma unidade Teste de teste amigável duplo ao lado de brincadeiras) para testes pesados de caso de borda, por exemplo, e depois escrever alguns testes de integração com o mecanismo de banco de dados real para estabelecer que os casos principais funcionam quando o sistema é montado.

Como uma nota secundária, você mencionou que as zombarias que você escreve simplesmente testam como algo é implementado, não se ele funciona . Isso é algo como um antipadrão: um teste que é um espelho perfeito de sua implementação não está realmente testando nada. Em vez disso, teste que cada classe ou método se comporta de acordo com sua própria especificação, em qualquer nível de abstração ou realismo que exija.

    
por 02.11.2015 / 20:39
fonte
87

One of my co-workers maintains that integration tests are all kinds of bad and wrong - everything must be unit-tested,

Isso é um pouco como dizer que os antibióticos são ruins - tudo deve ser curado com vitaminas.

Testes de unidade não conseguem capturar tudo - eles testam apenas como um componente funciona em um ambiente controlado . Os testes de integração verificam se tudo funciona juntos , o que é mais difícil de fazer, mas é mais significativo no final.

Um bom e abrangente processo de testes usa ambos 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.

Short of integration testing with a real connection object, how can I know that this is actually generating real queries - and that those queries actually do what I think they do?

Você poderia testar a unidade no nível do banco de dados . Execute a consulta com vários parâmetros e veja se você obtém os resultados esperados. Concedido, significa copiar / colar quaisquer alterações de volta ao código "verdadeiro". mas faz permitir que você teste a consulta independentemente de quaisquer outras dependências.

    
por 02.11.2015 / 18:03
fonte
15

Testes de unidade não detectam todos os defeitos. Mas eles são mais baratos de serem configurados e (re) executados em comparação com outros tipos de testes. Os testes unitários 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 testes.

source: p.470 no Código Completo 2 por McConnell

    
por 03.11.2015 / 05:23
fonte
12

Não, eles não são ruins. Espero que se deve ter testes de unidade e integração. Eles são usados e executados em diferentes estágios do ciclo de desenvolvimento.

Testes de unidade

Testes de unidade devem ser executados no servidor de compilação e localmente, após o código ter sido compilado. Se algum teste de unidade falhar, deve-se falhar a construção ou não confirmar a atualização de 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 seja capaz de executar todos os testes sem todas as dependências. Em seguida, poderíamos executar a compilação sem todas as dependências complexas necessárias e ter muitos testes executados com muita rapidez.

Então, para um banco de dados, deve-se ter algo como:

IRespository

List<Product> GetProducts<String Size, String Color);

Agora a implementação real do IRepository irá para o banco de dados para obter os produtos, mas para testes unitários, pode-se simular o IRepository com um falso para executar todos os testes conforme necessário sem um banco de dados actaul, pois podemos simular todos os tipos de listas de produtos que estão sendo retornados da instância de simulação e testam qualquer lógica de negócios com os dados de simulação.

Testes de integração

Os testes de integração são tipicamente testes de cruzamento de limites. Queremos executar esses testes no servidor de implantação (o ambiente real), no sandbox ou até mesmo localmente (apontados para o sandbox). Eles não são executados no servidor de compilação. Depois que o software foi implantado no ambiente, normalmente eles seriam executados como atividade de pós-implantação. Eles podem ser automatizados por meio de utilitários de linha de comando. Por exemplo, podemos executar o nUnit a partir da linha de comando se categorizarmos todos os testes de integração que queremos invocar. Eles realmente chamam o repositório real com a chamada real do banco de dados. Esses tipos de testes ajudam com:

  • Prontidão para estabilidade em saúde no ambiente
  • Testando a coisa real

Esses testes são, às vezes, mais difíceis de executar, já que precisamos configurar e / ou desmontar também. Considere adicionar um produto. Provavelmente, queremos adicionar o produto, consultá-lo para ver se ele foi adicionado e, depois disso, removê-lo. Não queremos adicionar 100s ou 1000s de produtos de "integração", portanto é necessária uma configuração adicional.

Os testes de integração podem ser valiosos para validar um ambiente e garantir que a coisa real funcione.

Um deve ter os dois.

  • Execute os testes de unidade para cada construção.
  • Execute os testes de integração para cada implementação.
por 02.11.2015 / 22:13
fonte
11

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 é bom. Você pode testar cada camada isoladamente zombando de camadas vizinhas, e isso também é bom. Mas não importa quantas camadas de abstração você crie, em algum momento tem que haver uma camada que faça o trabalho sujo - na verdade, fale com o banco de dados. A menos que você teste, você não testa nada. Se você testar a camada n pela camada de zombaria n-1 você está avaliando a suposição de que a camada n funciona sob condição de que camada n-1 funciona. Para que isso funcione, você deve provar que a camada 0 funciona.

Embora, em teoria, você possa testar o banco de dados de teste, analisando e interpretando o SQL gerado, é muito mais fácil e confiável criar um banco de dados de teste e conversar com ele.

Conclusão

Qual é a confiança obtida da unidade testando seu Repositório Abstrato , Ethereal Object-Relational-Mapper , Registro Ativo Genérico , Teórico Persistência camadas, quando no final o seu SQL gerado contém erro de sintaxe?

    
por 02.11.2015 / 23:14
fonte
6

Você precisa de ambos.

No seu exemplo, se você estivesse testando um banco de dados em uma determinada condição, quando o método findByKeyword for executado, você recuperará os dados, e espera que seja um teste de integração bem.

Em qualquer outro código que esteja usando o método findByKeyword , você deseja controlar o que está sendo alimentado no teste, para que possa retornar valores nulos ou as palavras corretas para o seu teste ou o que for que você escaneie a dependência do banco de dados. saber exatamente o que seu teste receberá (e você perderá a sobrecarga de se conectar a um banco de dados e garantir que os dados estejam corretos)

    
por 02.11.2015 / 18:07
fonte
6

O autor do artigo do blog ao qual você se refere é preocupa-se principalmente com a complexidade potencial que pode surgir de testes integrados (embora esteja 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 do que testes unitários puros. Isso realmente depende do contexto do seu aplicativo e do que você está tentando testar.

Muitas aplicações hoje simplesmente não funcionariam se o servidor de banco de dados estivesse inoperante. Pelo menos, pense nisso 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 de todo, no banco de dados, então escreva seu teste de tal forma que ele nem tente para usar o banco de dados (apenas forneça dados simulados, conforme necessário).  Por exemplo, se você estiver tentando testar alguma lógica de autenticação ao veicular uma página da Web (por exemplo), provavelmente é bom separá-la completamente do banco de dados (supondo que você não dependa do banco de dados para autenticação ou que você pode zombar razoavelmente facilmente).

Por outro lado, se é um recurso que confia diretamente em seu banco de dados e que não funcionaria em um ambiente real, o banco de dados não deveria estar disponível, então zombando do que o banco de dados faz em seu código de cliente de banco de dados. camada usando esse DB) não faz necessariamente sentido.

Por exemplo, se você souber que seu aplicativo vai depender de um banco de dados (e, possivelmente, de um sistema de banco de dados específico), zombar do comportamento do banco de dados com frequência resultará em perda de tempo. Mecanismos de banco de dados (especialmente RDBMS) são sistemas complexos. Algumas linhas de SQL podem executar muito trabalho, o que seria difícil de simular (na verdade, se sua consulta SQL tiver algumas linhas, provavelmente você precisará 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 mesmo.

Eu não necessariamente trataria isso como um problema de teste de unidade v.s. teste integrado , mas veja o escopo do que está sendo testado. Os problemas gerais de testes de unidade e integração permanecem: você precisa de um conjunto razoavelmente realista de dados de teste e casos de teste, mas algo que também seja suficientemente pequeno para que os testes sejam executados rapidamente.

O tempo para redefinir o banco de dados e preencher novamente com dados de teste é um aspecto a ser considerado; você geralmente avaliaria isso contra o tempo que leva para escrever esse código falso (que você teria que manter também, eventualmente).

Outro ponto a considerar é o grau de dependência que seu aplicativo tem com o banco de dados.

  • Se o seu aplicativo simplesmente segue um modelo CRUD, onde você tem uma camada de abstração que permite alternar entre qualquer RDBMS pelo simples meio de uma configuração, é possível trabalhar com um sistema simulado facilmente (possivelmente desfocar a linha entre a unidade e o teste integrado usando um RDBMS na memória).
  • Se o seu aplicativo usa uma lógica mais complexa, algo que seria específico do SQL Server, MySQL, PostgreSQL (por exemplo), geralmente faria mais sentido ter um teste que usasse esse sistema específico.
por 03.11.2015 / 15:04
fonte
1

Você está certo em considerar esse teste unitário incompleto. A incompletude está na interface do banco de dados que está sendo ridicularizada. A expectativa ou afirmações desse mock ingênuo são incompletas.

Para torná-lo completo, você teria que gastar tempo e recursos suficientes para escrever ou integrar um mecanismo de regras SQL que garantisse que a instrução SQL emitida pelo assunto em teste resultasse na expectativa operações.

No entanto, a alternativa / companheiro frequentemente esquecido e um tanto caro para zombar é "virtualização" .

Você pode criar 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 diversas visõ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.

    
por 03.11.2015 / 21:15
fonte
0

Unit Tests e Integration Tests são ortogonais entre si. Eles oferecem uma visão diferente do aplicativo que você está criando. Normalmente, você quer os dois . Mas o ponto no tempo é diferente, quando você quer que tipo de testes.

Na maioria das vezes, 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 de unit é deixado para o leitor. Mas o propósito é simples: obter feedback rápido de quando e onde seu código quebrou . Dito isso, deve ficar claro que as chamadas para um banco de dados real são um nono .

Por outro lado, há coisas que só podem ser testadas em unidades em condições difíceis sem um banco de dados. Talvez exista uma condição de corrida em seu código e uma chamada para um DB lança uma violação de um unique constraint que só poderia ser acionado se você realmente usasse seu sistema. Mas esses tipos de testes são caros e você não pode (e não quer) executá-los com tanta frequência quanto unit tests .

    
por 27.04.2016 / 20:01
fonte
0

No mundo do .Net, 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 para me desenvolver. Eu não estava tão interessado em executar todos os testes para cada compilação (porque isso diminui o fluxo de trabalho de desenvolvimento), mas eu entendo a utilidade disso para uma equipe maior. No entanto, você pode fazer uma regra antes de confirmar o código, todos os testes devem ser executados e aprovados (se demorar mais para os testes serem executados, porque o banco de dados está sendo realmente atingido).

Proibir a camada de acesso a dados (DAO) e não acessar o banco de dados, não apenas não permite que eu codifique como eu gosto e me acostumei, mas também perde um grande pedaço da base de código real. Se você não estiver realmente testando a camada de acesso a dados e o banco de dados e apenas fingindo e gastando muito tempo zombando das coisas, não consigo entender a utilidade dessa abordagem para realmente testar meu código. Estou testando um pequeno pedaço em vez de um maior com um teste. Eu entendo que minha abordagem pode ser mais semelhante a um teste de integração, mas parece que o teste de unidade com o mock é um desperdício redundante de tempo se você realmente apenas escrever o teste de integração uma vez e primeiro. Também é uma boa maneira de desenvolver e depurar.

Na verdade, há algum tempo, tenho conhecimento do TDD e Behavior Driven Design (BDD) e penso 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 muito mais completo e prioritário para escrever, que cobre mais código e é uma maneira mais eficiente de escrever testes.

Na verdade, acho que algo como Behavior Driven Design (BDD) que tenta testar de ponta a ponta com linguagem específica de domínio (DSL) deve ser o caminho a percorrer. Temos o SpecFlow no mundo do .Net, mas começou como código aberto com o Pepino.

link

Não estou realmente impressionado com a verdadeira utilidade do teste que escrevi, ridicularizando a 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 totalmente vazio que eu tinha que zombar de uma maneira não natural. Eu só acho que é uma perda de tempo.

De acordo com o Stack Overflow, o mocking é usado quando objetos reais são impraticáveis para serem incorporados ao teste de unidade.

link

"Mocking é usado principalmente em testes de unidade. Um objeto em teste pode ter dependências em outros objetos (complexos). Para isolar o comportamento do objeto que você deseja testar, substitua os outros objetos por simulados que simulem o comportamento do objeto." objetos reais. Isso é útil se os objetos reais são impraticáveis para incorporar no teste de unidade. "

Meu argumento é que se eu estou codificando qualquer coisa de ponta a ponta (web UI para camada de negócios para camada de acesso a banco de dados, ida e volta), antes de verificar qualquer coisa como desenvolvedor, vou testar essa ida e volta fluxo. Se eu cortar a interface do usuário e depurar e testar esse fluxo a partir de um teste, estou testando tudo menos da interface do usuário e retornando exatamente o que a interface do usuário espera. Tudo o que me resta é enviar à interface do usuário o que ela quer.

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 abrange o teste da especificação real do usuário, final a fim, tanto quanto possível. Se eu nunca criar quaisquer outros testes mais granulares, pelo menos eu tenho este mais um teste completo que prova que minha funcionalidade desejada funciona.

Um co-fundador da Stack Exchange não está convencido sobre os 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 atingisse o banco de dados ao manter um monte de simulações de banco de dados em qualquer dia.

link

    
por 09.03.2017 / 19:29
fonte
-1

Dependências externas devem ser ridicularizadas porque você não pode controlá-las (elas podem passar durante a fase de teste de integração, mas falhar na produção). As unidades podem falhar, as conexões do banco de dados podem falhar por vários motivos, pode haver problemas de rede, etc. Ter testes de integração não oferece nenhuma confiança extra, pois são todos os problemas que podem acontecer em tempo de execução.

Com verdadeiros testes de unidade, você está testando dentro dos limites da caixa de proteção e deve ficar claro. Se um desenvolvedor escreveu uma consulta SQL que falhou no QA / PROD, isso significa que eles nem mesmo testaram uma vez antes disso.

    
por 02.11.2015 / 20:26
fonte