Por que preciso de testes de unidade para testar métodos de repositório?

19

Preciso bancar um advogado do diabo nessa questão, porque não posso defendê-la bem por falta de experiência. Aqui está o acordo, eu recebo conceitualmente as diferenças entre teste de unidade e teste de integração. Ao se concentrar especificamente nos métodos de persistência e no repositório, um teste de unidade usaria uma simulação, possivelmente por meio de uma estrutura como o Moq, para afirmar que o pedido pesquisado foi retornado conforme o esperado.

Digamos que eu criei o seguinte teste de unidade:

[TestMethod]
public void GetOrderByIDTest()
{
   //Uses Moq for dependency for getting order to make sure 
   //ID I set up in 'Arrange' is same one returned to test in 'Assertion'
}

Portanto, se eu configurar OrderIdExpected = 5e meu objeto simulado retornar 5como o ID, meu teste passará. Entendi. Eu testei o código da unidade para garantir que o meu código pré-formado retorne o objeto e o ID esperados e não outra coisa.

O argumento que receberei é o seguinte:

"Por que não pular os testes de unidade e fazer testes de integração? É importante testar o procedimento armazenado do banco de dados e seu código. Parece muito trabalho extra ter testes de unidade e testes de integração quando, no final das contas, quero saber se o banco de dados chama e o código funciona. Sei que os testes levam mais tempo, mas precisam ser executados e testados independentemente, portanto, para mim parece inútil ter os dois. Apenas teste o que importa. "

Eu poderia defendê-lo com uma definição de livro de texto como: "Bem, isso é um teste de integração e precisamos testar o código separadamente como um teste de unidade e, yada, yada, yada ..." Esse é um caso em que uma explicação purista das práticas vs. a realidade está perdendo. Às vezes me deparo com isso e, se não consigo defender o raciocínio por trás do código de teste de unidade que, em última análise, depende de dependências externas, não posso argumentar.

Qualquer ajuda sobre esta questão é muito apreciada, obrigado!

atconway
fonte
2
i dizer enviá-lo direto para testes de usuários ... eles estão indo para alterar os requisitos de qualquer maneira ...
Nathan hayfield
+1 para o sarcasmo para manter as coisas leves ao longo do tempo
atconway
2
A resposta simples é que cada método pode ter um número x de casos extremos (digamos que você queira testar IDs positivos, ID de 0 e IDs negativos). Se você quiser testar alguma funcionalidade que use esse método, que possui 3 casos de borda, você precisará escrever 9 casos de teste para testar cada combinação de casos de borda. Ao isolá-los, você só precisa escrever 6. Além disso, os testes oferecem uma idéia mais específica do motivo pelo qual algo ocorreu. Talvez seu repositório retorne nulo em caso de falha e a exceção nula seja lançada várias centenas de linhas no código.
28515 Rob
Eu acho que você está sendo muito rigoroso em sua definição de teste "unitário". O que é uma "unidade de trabalho" para uma classe de repositório?
Calebe
E certifique-se de considerar o seguinte: se seu teste de unidade zomba de tudo o que você está tentando testar, o que você está realmente testando?
Calebe

Respostas:

20

Testes de unidade e testes de integração têm finalidades diferentes.

Os testes de unidade verificam a funcionalidade do seu código ... que você obtém o que espera do método quando o chama. Os testes de integração testam como o código se comporta quando combinado como um sistema. Você não espera que os testes de unidade avaliem o comportamento do sistema, nem que os testes de integração verifiquem saídas específicas de um método específico.

Os testes de unidade, quando feitos corretamente, são mais fáceis de configurar do que os testes de integração. Se você confiar apenas em testes de integração, seu teste será:

  1. Seja mais difícil escrever, no geral,
  2. Seja mais frágil, devido a todas as dependências necessárias e
  3. Ofereça menos cobertura de código.

Conclusão: use o teste de integração para verificar se as conexões entre os objetos estão funcionando corretamente, mas use o teste de unidade primeiro para verificar os requisitos funcionais.


Tudo isso dito, talvez você não precise de testes de unidade para certos métodos de repositório. O teste de unidade não deve ser realizado em métodos triviais ; se tudo o que você está fazendo é passar por uma solicitação de um objeto para o código gerado pelo ORM e retornar um resultado, não é necessário fazer o teste de unidade na maioria dos casos; um teste de integração é adequado.

Robert Harvey
fonte
Sim, obrigado pela resposta rápida, eu agradeço. Minha única crítica à resposta é que ela cai na preocupação do meu último parágrafo. Minha oposição ainda ouvirá explicações e definições abstratas e não um raciocínio mais profundo. Por exemplo, você poderia fornecer um raciocínio aplicado ao meu caso de uso de teste do procedimento armazenado / banco de dados em relação ao meu código e por que os testes de unidade ainda são valiosos em referência a esse caso de uso específico ?
Atletway
1
Se o SP está retornando um resultado do banco de dados, um teste de integração pode ser adequado. Se houver uma lógica não trivial, são indicados testes de unidade. Consulte também stackoverflow.com/questions/1126614/… e msdn.microsoft.com/en-us/library/aa833169(v=vs.100).aspx (específico do SQL Server).
Robert Harvey
12

Eu estou do lado dos pragmáticos. Não teste seu código duas vezes.

Nós escrevemos apenas testes de integração para nossos repositórios. Esses testes dependem apenas de uma configuração de teste simples que é executada em um banco de dados na memória. Eu acho que eles fornecem tudo o que um teste de unidade faz e muito mais.

  1. Eles podem substituir testes de unidade ao fazer TDD. Embora exista um pouco mais de clichê de código de teste para escrever antes que você possa começar com o código real, ele funciona muito bem com a abordagem de vermelho / verde / refator quando tudo estiver no lugar.
  2. Eles testam o código real do repositório - o código contido nas strings SQL ou nos comandos ORM. É mais importante verificar se a consulta está correta do que verificar se você realmente enviou alguma sequência para um StatementExecutor.
  3. Eles são excelentes como testes de regressão. Se eles falharem, é sempre devido a um problema real, como uma alteração no esquema que não foi contabilizada.
  4. Eles são indispensáveis ​​ao alterar o esquema do banco de dados. Você pode ter certeza de que não alterou o esquema de uma maneira que interrompa seu aplicativo enquanto o teste passar. (O teste de unidade é inútil nesse caso, porque quando o procedimento armazenado não existir mais, o teste de unidade ainda será aprovado.)
waxwing
fonte
Muito pragmático mesmo. Eu experimentei o caso em que o banco de dados na memória realmente se comporta diferente (não em termos de desempenho) do meu banco de dados real, devido ao ORM ligeiramente diferente. Cuide disso em seus testes de integração.
Marcel
1
@ Marcel, eu já me deparei com isso. Eu o resolvia algumas vezes executando todos os meus testes em um banco de dados real.
Winston Ewert
4

Os testes de unidade fornecem um nível de modularidade que os testes de integração (por projeto) não podem. Quando um sistema é reformulado ou recomposta, (e isso vai acontecer) testes de unidade pode muitas vezes ser reutilizado, enquanto os testes de integração deve muitas vezes ser re-escrita. Testes de integração que tentam fazer o trabalho de algo que deveria estar em um teste de unidade geralmente fazem muito, o que dificulta a manutenção.

Além disso, a inclusão de testes de unidade tem os seguintes benefícios:

  1. Os testes de unidade permitem decompor rapidamente um teste de integração com falha (possivelmente devido a um erro de regressão) e identificar a causa. Além disso, isso comunicará o problema a toda a equipe mais rapidamente do que diagramas ou outra documentação.
  2. Os testes de unidade podem servir como exemplos e documentação (um tipo de documentação que realmente compila ) junto com o seu código. Ao contrário de outras documentações, você saberá instantaneamente quando estiver desatualizado.
  3. Os testes de unidade podem servir como indicadores de desempenho da linha de base ao tentar decompor problemas de desempenho maiores, enquanto os testes de integração tendem a exigir muita instrumentação para encontrar o problema.

É possível dividir seu trabalho (e aplicar uma abordagem DRY) enquanto você usa os testes de unidade e integração. Simplesmente confie nos testes de unidade para pequenas unidades de funcionalidade e não repita nenhuma lógica que já esteja em um teste de unidade em um teste de integração. Isso geralmente leva a menos trabalho (e, portanto, menos re-trabalho).

Zachary Yates
fonte
4

Os testes são úteis quando são interrompidos: proativamente ou reativamente.

Os testes de unidade são proativos e podem ser uma verificação funcional contínua, enquanto os testes de integração são reativos em dados faseados / falsos.

Se o sistema em consideração estiver próximo / dependente dos dados mais do que da lógica computacional, os Testes de Integração deverão ganhar mais importância.

Por exemplo, se estivermos construindo um sistema ETL, os testes mais relevantes serão em torno de dados (faseados, falsificados ou ativos). O mesmo sistema terá alguns testes de unidade, mas apenas em torno de validação, filtragem etc.

O padrão do repositório não deve ter lógica computacional ou comercial. É muito próximo ao banco de dados. Mas o repositório também está próximo da lógica de negócios que o utiliza. Portanto, é uma questão de encontrar um BALANÇO.

Os testes de unidade podem testar como o repositório se comporta. Os testes de integração podem testar o que realmente aconteceu quando o repositório foi chamado.

Agora, "O QUE REALMENTE ACONTECEU" pode parecer muito atraente. Mas isso pode ser muito caro para executar de forma consistente e repetida. A definição clássica de testes quebradiços se aplica aqui.

Portanto, na maioria das vezes, acho bom o suficiente escrever o teste de unidade em um objeto que usa o repositório. Dessa forma, testamos se o método de repositório apropriado foi chamado e os resultados simulados adequados são retornados.

Otpidus
fonte
Eu gosto do seguinte: "Agora," O QUE REALMENTE ACONTECEU "pode ​​parecer muito atraente. Mas isso pode ser muito caro para executar de forma consistente e repetida".
Atletway
2

Há três itens separados que precisam ser testados: o procedimento armazenado, o código que chama o procedimento armazenado (por exemplo, sua classe de repositório) e o consumidor. A tarefa do repositório é gerar uma consulta e converter o conjunto de dados retornado em um objeto de domínio. Há código suficiente para oferecer suporte ao teste de unidade que se separa da execução real da consulta e da criação do conjunto de dados.

Então (este é um exemplo muito, muito simplificado):

interface IOrderRepository
{
    Order GetOrderByID(Guid id);
}

class OrderRepository : IOrderRepository
{
    private readonly ISqlExecutor sqlExecutor;
    public OrderRepository(ISqlExecutor sqlExecutor)
    {
        this.sqlExecutor = sqlExecutor;
    }

    public Order GetOrderByID(Guid id)
    {
        var sql = "SELECT blah, blah FROM Order WHERE OrderId = @p0";
        var dataset = this.sqlExecutor.Execute(sql, p0 = id);
        var result = this.orderFromDataset(dataset);
        return result;
    }
}

Em seguida, ao testar OrderRepository, passe um mocked ISqlExecutore verifique se o objeto em teste passa no SQL correto (esse é o seu trabalho) e retorna um Orderobjeto apropriado, considerando alguns conjuntos de dados de resultados (também mocked). Provavelmente, a única maneira de testar a SqlExecutorclasse concreta é com testes de integração, justos o suficiente, mas essa é uma classe de invólucro fino e raramente muda, então é grande coisa.

Você ainda precisa testar seus procedimentos armazenados também.

Scott Whitlock
fonte
0

Posso reduzir isso para uma linha de pensamento muito simples: favorecer os testes de unidade porque ajuda a localizar bugs. E faça isso de forma consistente, porque inconsistências causam confusão.

Outras razões derivam das mesmas regras que orientam o desenvolvimento de OO, uma vez que também se aplicam a testes. Por exemplo, o princípio da responsabilidade única.

Se alguns testes de unidade parecem que não valem a pena, talvez seja um sinal de que o assunto não vale a pena. Ou talvez sua funcionalidade seja mais abstrata que sua contraparte e os testes para ela (assim como seu código) possam ser abstraídos para um nível superior.

Como com qualquer coisa, há exceções. E a programação ainda é uma forma de arte; muitos problemas são diferentes o suficiente para justificar a avaliação de cada um para obter a melhor abordagem.

CL22
fonte