Criando testes de unidade em uma camada CRUD de um aplicativo, como posso tornar os testes independentes?

14

Portanto, estou tentando fazer meus testes de unidade o mais simples possível, mas isso se torna problemático quando estou testando alguns métodos simples de Adicionar / Excluir.

Para o método add, eu basicamente tenho que criar um objeto fictício e adicioná-lo; depois que o teste for bem-sucedido, eu tenho que excluir o objeto fictício.

E para o teste de exclusão, obviamente tenho que criar um objeto fictício para poder excluí-lo.

Como você pode ver se um teste falha, o outro também falha, pois ambos são necessários.

O mesmo acontece com um sistema em que você precisaria escrever um teste que "Cancela uma ordem" ... bem, seria necessária uma ordem fictícia para cancelar primeiro, isso não é contrário às diretrizes do teste de unidade?

Como casos como esse devem ser tratados?

Kyle
fonte
Você também pode dar uma olhada nesta pergunta: programmers.stackexchange.com/questions/115455/…
Guven

Respostas:

13

Bem, não há nada errado com o que você está fazendo. Vários testes podem cobrir o mesmo código; significa apenas que um problema fará com que vários testes falhem. O que você deseja evitar são testes que dependem dos resultados de outros testes. Ou seja, seu teste de exclusão depende da execução do teste de adição e, portanto, se você executar o teste de exclusão ANTES de adicionar, ele falhará. Para evitar esse problema, verifique se você tem uma "folha em branco" no início de cada teste, para que o que acontece em um determinado teste não possa afetar os testes subsequentes.

Uma ótima maneira de fazer isso é executar os testes em um banco de dados na memória.

No seu teste de adição, crie um banco de dados vazio, adicione o objeto e afirme que ele realmente foi adicionado.

No seu teste de exclusão, crie o banco de dados com o objeto que você já estará excluindo. Exclua o objeto e afirme que ele foi excluído.

Sopre o banco de dados em seu código desmontável.

Adam Jaskiewicz
fonte
O banco de dados na memória é rápido (na memória) e simples (no processo). Você poderia fazer isso com qualquer armazenamento de dados.
Paul Draper
3

Use transações.

Se você estiver usando um banco de dados que suporta transações, execute cada teste em uma transação. Retroceda a transação no final do teste. Cada teste deixará o banco de dados inalterado.

Sim, para cancelar um pedido, você primeiro precisará criar um. Isso é bom. O teste primeiro cria um pedido, depois o cancela e verifica se o pedido foi cancelado.

Kevin Cline
fonte
Amo essa ideia. Implementado com grande efeito hoje.
Pimbrouwers
3

Você está indo bem. O primeiro e único princípio fundamental do teste de unidade é cobrir todos os caminhos de código que você possui, para que você possa ter certeza de que seu código está fazendo o que deveria e continua fazendo isso depois de alterações e refatorações. Manter os testes de unidade pequenos, simples e de uso único é uma meta que vale a pena, mas não é fundamental. Ter um teste que chame dois métodos de relacionar sua API não é, por si só, questionável; na verdade, como você ressalta, geralmente é necessário. A desvantagem de ter testes redundantes é que eles levam mais tempo para escrever, mas como quase tudo em desenvolvimento, essa é uma troca que você precisa fazer o tempo todo, e a melhor solução quase nunca é um dos pontos extremos.

Kilian Foth
fonte
2

As técnicas de teste de software são extremamente variadas e, quanto mais você se instruir sobre elas, começará a ver muitas orientações diferentes (e às vezes conflitantes). Não há um único livro para seguir.

Eu acho que você está em uma situação em que você viu algumas orientações para testes de unidade que dizem coisas como

  • Cada teste deve ser autônomo e não ser afetado por outros testes
  • Cada teste de unidade deve testar uma coisa, e apenas uma coisa
  • Os testes de unidade não devem atingir o banco de dados

e assim por diante. E tudo isso está certo, dependendo de como você define 'teste de unidade' .

Eu definiria um 'teste de unidade' como algo como: "um teste que exercita uma parte da funcionalidade de uma unidade de código, isolada de outros componentes dependentes".

Sob essa definição, o que você está fazendo (se precisar adicionar um registro a um banco de dados antes de poder executar o teste) não é um 'teste de unidade', mas mais do que é comumente chamado de 'teste de integração'. (Pela minha definição, um verdadeiro teste de unidade não atingirá o banco de dados; portanto, você não precisará adicionar um registro antes de excluí-lo.)

Um teste de integração exercitará a funcionalidade que usa vários componentes (como uma interface com o usuário e um banco de dados), e as orientações que se aplicariam aos testes de unidade não se aplicam necessariamente aos testes de integração.

Como outras pessoas mencionaram em suas respostas, o que você está fazendo não é necessariamente errado, mesmo que você faça coisas contrárias a algumas orientações de teste de unidade. Em vez disso, tente argumentar sobre o que você está realmente testando em cada método de teste e, se achar que precisa de vários componentes para satisfazer seu teste, e alguns componentes exigem pré-configuração, vá em frente e faça-o.

Mas, acima de tudo, entenda que existem muitos tipos de testes de software (testes de unidade, testes de sistema, testes de integração, testes exploratórios etc.) e não tente aplicar a orientação de um tipo a todos os outros.

Eric King
fonte
Então você está dizendo que não pode excluir o teste de unidade do banco de dados?
ChrisF
Se você está acessando o banco de dados, é (por definição) um teste de integração, não um teste de unidade. Então, nesse sentido, não. Você não pode excluir o 'teste de unidade' de um banco de dados. O que você pode fazer no teste de unidade é que, quando o código que você está testando é solicitado a excluir alguns dados, ele interage com o módulo de acesso a dados corretamente.
Eric King
Mas o ponto é que algumas pessoas podem definir 'teste de unidade' de maneira diferente; portanto, devemos ter cuidado ao aplicar a orientação de 'teste de unidade', pois a orientação pode não se aplicar da maneira que achamos que é.
Eric King
1

É exatamente por isso que uma das outras diretrizes é usar interfaces. Se você usar um objeto que implementa uma interface em vez de uma implementação de classe específica, você pode criar uma classe que não dependa do restante da base de código.

Outra alternativa é usar uma estrutura de simulação. Isso permite que você crie facilmente esses tipos de objetos fictícios que podem ser passados ​​para o método que você está testando. É possível que você precise criar algumas implementações de stub para a classe dummy, mas ele ainda cria uma separação da implementação real e com o que o teste está relacionado.

unholysampler
fonte
1

Como você pode ver se um teste falha, o outro também falha, pois ambos são necessários.

Então?

... isso não vai de encontro às diretrizes do teste de unidade?

Não.

Como casos como esse devem ser tratados?

Vários testes podem ser independentes e todos falham devido ao mesmo bug. Isso é realmente normal. Muitos testes podem - indiretamente - testar algumas funcionalidades comuns. E tudo falha quando a funcionalidade comum é interrompida. Nada de errado com isso.

Os testes de unidade são definidos como classes precisamente para que eles possam compartilhar código facilmente, como um registro fictício comum usado para testar a atualização e a exclusão.

S.Lott
fonte
1

Você pode usar uma estrutura simulada ou um 'ambiente' com um banco de dados na memória. A última é uma classe em que você pode criar tudo o que precisa para fazer o teste passar, antes da execução do teste.

Prefiro o último - os usuários podem ajudá-lo a inserir alguns dados para que seus testes se tornem mais próximos do mundo real.

Andre
fonte
Verdadeiro - mas você não está realmente testando a conexão real com o banco de dados aqui. A menos que você assuma que isso sempre funcionará - mas as suposições são perigosas.
ChrisF