Os testes de unidade não devem usar meus próprios métodos?


83

Hoje eu estava assistindo um vídeo "Noções básicas de JUnit " e o autor disse que ao testar um determinado método em seu programa, você não deve usar outros métodos no processo.

Para ser mais específico, ele estava falando sobre o teste de algum método de criação de registros que utilizou um nome e um sobrenome para argumentos, e os usou para criar registros em uma determinada tabela. Mas ele alegou que, no processo de teste desse método, ele não deveria usar seus outros métodos DAO para consultar o banco de dados para verificar o resultado final (para verificar se o registro foi realmente criado com os dados corretos). Ele afirmou que, para isso, ele deve escrever código JDBC adicional para consultar o banco de dados e verificar o resultado.

Acho que entendo o espírito de sua afirmação: você não deseja que o caso de teste de um método dependa da exatidão dos outros métodos (neste caso, o método DAO), e isso é realizado escrevendo (novamente) sua própria validação / código de suporte (que deve ser mais específico e focado, portanto, código mais simples).

No entanto, vozes dentro da minha cabeça começaram a protestar com argumentos como duplicação de código, esforços adicionais desnecessários, etc. Quero dizer, se rodarmos toda a bateria de teste e testarmos todos os nossos métodos públicos exaustivamente (incluindo o método DAO neste caso), não devemos não há problema em usar apenas alguns desses métodos ao testar outros métodos? Se um deles não estiver fazendo o que deveria, seu próprio caso de teste falhará e podemos corrigi-lo e executar a bateria de teste novamente. Não há necessidade de duplicação de código (mesmo que o código duplicado seja um pouco mais simples) ou esforços desperdiçados.

Eu tenho um forte pressentimento sobre isso por causa de vários aplicativos Excel - VBA recentes que eu escrevi (testados adequadamente pela unidade graças ao Rubberduck for VBA ), onde aplicar essa recomendação significaria muito trabalho extra adicional, sem benefício percebido.

Você pode compartilhar suas idéias sobre isso?


79
É um pouco estranho ver um teste de unidade que envolve o banco de dados em tudo
Richard Tingle

4
Na IMO, é bom chamar outras classes de IFF, elas são rápidas o suficiente. Zombe de qualquer coisa que precise ir para o disco ou pela rede. Não faz sentido zombar de uma IMO simples e de classe.
precisa saber é o seguinte

2
Tem um link para este vídeo?
Candied_orange # 6/16

17
" testado adequadamente, graças à Rubberduck para VBA " - senhor, acabou de fazer o meu dia. Eu consertava o erro e o editava de "RubberDuck" para "Rubberduck", mas me sinto como se algum spammer fizesse isso e adicionasse um link para rubberduckvba.com (eu sou o dono do nome de domínio e co-proprietário do projeto com @RubberDuck) - então, apenas comentarei aqui. De qualquer forma, é incrível ver as pessoas usando a ferramenta responsável pela maior parte das minhas noites sem dormir pela maior parte dos últimos dois anos! =)
Mathieu Guindon

4
@ Mat'sMug e RubberDuck Adoro o que você está fazendo com Rubberduck. Por favor, continue assim. Certamente minha vida é mais fácil por causa disso (por acaso faço muitos pequenos programas e protótipos no Excel VBA). BTW, a menção Rubberduck no meu post foi apenas eu tentando ser legal com o próprio RubberDuck, o que por sua vez foi legal comigo aqui no PE. :)
carlossierra

Respostas:


186

O espírito de sua afirmação é realmente correto. O objetivo dos testes de unidade é isolar o código, testá-lo livre de dependências, para que qualquer comportamento incorreto possa ser rapidamente reconhecido onde está acontecendo.

Com isso dito, o teste de unidade é uma ferramenta, e serve para seus propósitos, não é um altar para o qual se orar. Às vezes, isso significa deixar dependências porque elas funcionam de maneira confiável o suficiente e você não quer incomodá-las. Às vezes, isso significa que alguns de seus testes de unidade são realmente muito próximos, se não realmente de integração.

No final das contas, você não está sendo avaliado, o importante é o produto final do software que está sendo testado, mas você só deve ter em mente quando está violando as regras e decidindo quando as compensações valem a pena.


117
Viva o pragmatismo.
Robert Harvey

6
Eu acrescentaria que isso não é tanto a confiabilidade quanto a velocidade que é o problema que leva ao stubbing de dependências. O teste no mantra de isolamento é tão convincente, no entanto, que esse argumento geralmente é esquecido.
Michael Durrant

4
"... o teste de unidade é uma ferramenta, e serve para seus propósitos, não é um altar para se orar." - isto!
Wayne Conrad

15
"não um altar para se rezar" - treinadores irritados do TDD chegando!
Den

5
@ jpmc26: pior ainda, você não deseja que todos os seus testes de unidade sejam aprovados porque eles manipularam corretamente o serviço da web, o que é um comportamento válido e correto, mas nunca realmente exercitando o código para quando ele estiver pronto! Depois de stub-lo, você pode escolher se é "up" ou "down" e testar os dois.
Steve Jessop

36

Eu acho que isso se resume a terminologia. Para muitos, um "teste de unidade" é algo muito específico e, por definição, não pode ter uma condição de aprovação / reprovação que depende de qualquer código fora da unidade (método, função etc.) sendo testado. Isso incluiria interação com um banco de dados.

Para outras pessoas, o termo "teste de unidade" é muito mais flexível e abrange qualquer tipo de teste automatizado, incluindo código de teste que testa partes integradas do aplicativo.

Um purista de teste (se é que posso usar esse termo) pode chamar isso de teste de integração , diferenciado de um teste de unidade com base no fato de que o teste depende de mais de uma unidade pura de código.

Eu suspeito que você esteja usando a versão mais flexível do termo "teste de unidade" e na verdade esteja se referindo a um "teste de integração".

Usando essas definições, você não deve depender do código fora da unidade em teste em um "teste de unidade". Em um "teste de integração", no entanto, é perfeitamente razoável escrever um teste que exercite algum trecho de código específico e depois inspecione o banco de dados quanto aos critérios de aprovação.

Se você deve depender de testes de integração ou de unidade ou de ambos é um tópico muito maior de discussão.


Você está certo em suas suspeitas. Estou usando mal os termos e agora posso ver como cada tipo de teste pode ser tratado de maneira mais apropriada. Obrigado!
precisa saber é o seguinte

11
"Para outros, o termo" teste de unidade "é muito mais flexível" - além de qualquer outra coisa, as chamadas "estruturas de teste de unidade" são uma maneira realmente conveniente de organizar e executar testes de nível superior ;-)
Steve Jessop,

11
Em vez de uma distinção "pura" vs "frouxa", carregada de conotações, eu sugiro que a distinção seja entre aqueles que visualizam "unidades", "dependências" etc. de uma perspectiva de domínio (por exemplo, "autenticação" contaria como uma unidade, "banco de dados" contaria como uma dependência, etc.) vs. aqueles que os visualizam da perspectiva da linguagem de programação (por exemplo, métodos são unidades, classes são dependências). Acho que definir o que se quer dizer com frases como "a unidade em teste" pode transformar argumentos ("Você está errado!") Em discussões ("Este é o nível certo de granularidade?").
Warbo 8/09/16

11
@EricKing Obviamente, para a grande maioria das pessoas da sua primeira categoria, a própria idéia de envolver o banco de dados em todos os testes de unidade é um anátema, seja você usar seu DAOs ou acessar diretamente o banco de dados.
Periata Breatta

11
@AmaniKilumanga A pergunta era basicamente "alguém diz que eu não devo fazer isso em testes de unidade, mas faço em testes de unidade e acho que está bem. Está tudo bem?" E minha resposta foi "Sim, mas a maioria das pessoas chamaria isso de teste de integração em vez de teste de unidade". Quanto à sua pergunta, não sei o que você quer dizer com "os testes de integração podem usar métodos de código de produção?".
Eric King

7

A resposta é sim e não...

Testes exclusivos que são executados isoladamente são uma necessidade absoluta, pois permite estabelecer o trabalho básico de que seu código de baixo nível está funcionando adequadamente. Porém, ao codificar bibliotecas maiores, você também encontrará áreas de código que exigirão que você tenha testes que cruzam unidades.

Esses testes entre unidades são bons para cobertura de código e ao testar a funcionalidade de ponta a ponta, mas eles trazem algumas desvantagens que você deve estar ciente:

  • Sem um teste isolado para confirmar o que está ocorrendo, um teste "entre unidades" com falha exigirá solução de problemas adicional para determinar exatamente o que está errado com seu código
  • Confiar demais em testes entre unidades pode tirar você da mentalidade de contrato em que você sempre deveria estar ao escrever um código orientado a objeto do SOLID. Testes isolados normalmente fazem sentido porque suas unidades devem executar apenas uma ação básica.
  • O teste de ponta a ponta é desejável, mas pode ser perigoso se um teste exigir que você grave em um banco de dados ou execute alguma ação que você não gostaria que ocorresse em um ambiente de produção. Essa é uma das muitas razões pelas quais zombar de estruturas como o Mockito são tão populares porque permite falsificar um objeto e imitar um teste de ponta a ponta sem alterar algo que você não deveria.

No final do dia, você deseja ter alguns dos dois .. muitos testes que prendem a funcionalidade de baixo nível e alguns que testam a funcionalidade de ponta a ponta. Concentre-se na razão pela qual você está escrevendo testes em primeiro lugar. Eles estão lá para lhe dar confiança de que seu código está executando da maneira que você espera. Tudo o que você precisa fazer para que isso aconteça é bom.

O fato triste, porém verdadeiro, é que, se você estiver usando testes automatizados, já terá muitos desenvolvedores. Boas práticas de teste são a primeira coisa a deslizar quando uma equipe de desenvolvimento enfrenta prazos difíceis. Desde que você mantenha suas armas e escreva testes de unidade que são um reflexo significativo de como o seu código deve executar, eu não ficaria obcecado com o quão "puros" são seus testes.


4

Um problema com o uso de outros métodos de objeto para testar uma condição é que você perderá erros que se cancelam. Mais importante, você está perdendo a dor de uma implementação de teste difícil, e essa dor está lhe ensinando algo sobre o código subjacente. Testes dolorosos agora significam manutenção dolorosa mais tarde.

Diminua suas unidades, divida sua classe, refatore e refaça o design até que seja mais fácil testar sem reimplementar o restante do seu objeto. Obtenha ajuda se achar que seu código ou testes não podem mais ser simplificados. Tente pensar em um colega que parece sempre ter sorte e obtenha tarefas fáceis de testar de maneira limpa. Peça ajuda a ele, porque não é sorte.


11
A chave aqui está na unidade. Se você precisar fazer chamadas para outros procedimentos e processos, não seria, para mim, uma unidade. Se várias chamadas forem feitas e várias etapas precisarem ser validadas, se for um código grande e não uma unidade, divida-o em pedaços menores, cobrindo um único pedaço de lógica. Normalmente, uma chamada ao banco de dados também seria zombada ou um banco de dados na memória seria usado no teste de unidade para acessar um banco de dados real e precisar desfazer dados após o teste.
dlb 6/09/16

1

Se a unidade de funcionalidade que está sendo testada é 'os dados armazenados persistentemente e recuperáveis', então eu faria o teste de unidade que - armazena em um banco de dados real, destrói quaisquer objetos que contenham referências ao banco de dados e chame o código para buscar o objetos de volta.

Testar se um registro é criado no banco de dados parece estar preocupado com os detalhes da implementação do mecanismo de armazenamento, em vez de testar a unidade de funcionalidade exposta ao restante do sistema.

Convém atalho para o teste para melhorar o desempenho usando um banco de dados falso, mas tive contratados que fizeram exatamente isso e saíram no final do contrato com um sistema que passou nesses testes, mas na verdade não armazenou nada no banco de dados entre as reinicializações do sistema.

Você pode argumentar se 'unidade' no teste de unidade denota uma 'unidade de código' ou 'unidade de funcionalidade', funcionalidade talvez criada por várias unidades de código em conjunto. Não acho essa distinção útil - as perguntas que eu tenho em mente são 'o teste diz algo sobre se o sistema fornece valor comercial' e 'o teste é frágil se a implementação mudar?' Testes como o descrito são úteis quando você está executando o TDD no sistema - você ainda não escreveu o 'objeto get do registro do banco de dados' e, portanto, não pode testar a unidade completa da funcionalidade - mas é frágil contra as alterações na implementação, então eu remova-os assim que a operação completa for testável.


1

O espírito está correto.

Idealmente, em um teste de unidade , você está testando uma unidade (um método individual ou classe pequena).

Idealmente, você excluiria todo o sistema de banco de dados. Ou seja, você executaria seu método em um ambiente falso e apenas certifique-se de que ele chame as APIs de banco de dados corretas na ordem correta. Você explicitamente, positivamente, não deseja testar o banco de dados ao testar um de seus próprios métodos.

Os benefícios são muitos. Acima de tudo, os testes ficam ofuscantes rapidamente porque você não precisa se preocupar em configurar um ambiente de banco de dados correto e revertê-lo posteriormente.

Obviamente, esse é um objetivo elevado em qualquer projeto de software que não faça isso desde o início. Mas é o espírito do teste de unidade .

Observe que existem outros testes, como testes de recursos, testes de comportamento etc. que são diferentes dessa abordagem - não confunda "teste" com "teste de unidade".


"apenas verifique se ele chama as APIs de banco de dados corretas na ordem correta" - sim, claro. Até que você tenha interpretado mal a documentação e a ordem correta real que deveria estar usando é completamente diferente. Não zombe de sistemas fora do seu controle.
Joker_vD

4
@Joker_vD É para isso que servem os testes de integração. Nos testes de unidade, os sistemas externos absolutamente devem ser ridicularizados.
Ben Aaronson

1

Há pelo menos um motivo muito importante para não usar o acesso direto ao banco de dados em seus testes de unidade para o código comercial que interage com o banco de dados: se você alterar a implementação do banco de dados, terá que reescrever todos esses testes de unidade. Renomeie uma coluna, para o seu código, basta alterar uma única linha na sua definição do mapeador de dados. Se você não usar seu mapeador de dados durante o teste, no entanto, também precisará alterar todos os testes de unidade que fazem referência a esta coluna . Isso pode se transformar em uma quantidade extraordinária de trabalho, principalmente para alterações mais complexas que são menos passíveis de busca e substituição.

Além disso, o uso do seu mapeador de dados como um mecanismo abstrato, em vez de falar diretamente com o banco de dados, facilita a remoção total da dependência do banco de dados , o que pode não ser relevante agora, mas quando você realiza milhares de testes de unidade e todos eles ao acessar um banco de dados, você será grato por poder refatorá-los facilmente para remover essa dependência, pois seu conjunto de testes deixará de levar alguns minutos para rodar para segundos, o que pode ter um enorme benefício em sua produtividade.

Sua pergunta indica que você está pensando na linha certa. Como você suspeita, os testes de unidade também são códigos. É tão importante torná-los fáceis de manter e adaptar-se a mudanças futuras quanto no restante da sua base de código. Esforce-se para mantê-los legíveis e eliminar a duplicação, e você terá um conjunto de testes muito melhor para isso. E um bom conjunto de testes é aquele que é usado. E um conjunto de testes que é usado ajuda a encontrar erros, enquanto um que não é usado é apenas inútil.


1

A melhor lição que aprendi ao aprender sobre teste de unidade e teste de integração é não testar métodos, mas testar comportamentos. Em outras palavras, o que esse objeto faz?

Quando olho dessa maneira, um método que persiste em dados e outro método que os lê de volta começa a ser testável. Se você está testando os métodos especificamente, é claro, você acaba com um teste para cada método isoladamente, como:

@Test
public void canSaveData() {
    writeDataToDatabase();
    // what can you assert - the only expectation you can have here is that an exception was not thrown.
}

@Test
public void canReadData() {
    // how do I even get data in there to read if I cannot call the method which writes it?
}

Esse problema ocorre devido à perspectiva dos métodos de teste. Não teste métodos. Testar comportamentos. Qual é o comportamento da classe WidgetDao? Persiste em widgets. Ok, como você verifica se os widgets persistem? Bem, qual é a definição de persistência? Isso significa que, quando você escreve, pode lê-lo novamente. Portanto, leia + escreva junto se torne um teste e, na minha opinião, um teste mais significativo.

@Test
public void widgetsCanBeStored() {
    Widget widget = new Widget();
    widget.setXXX.....
    // blah

    widgetDao.storeWidget(widget);
    Widget stored = widgetDao.getWidget(widget.getWidgetId());
    assertEquals(widget, stored);
}

Esse é um teste lógico, coeso, confiável e, na minha opinião, significativo.

As outras respostas focam a importância do isolamento, o debate pragmático versus realista, e se um teste de unidade pode ou não consultar um banco de dados. Aqueles realmente não respondem à pergunta.

Para testar se algo pode ser armazenado, você deve armazená-lo e depois lê-lo novamente. Você não pode testar se algo está armazenado se não tiver permissão para lê-lo. Não teste o armazenamento de dados separadamente da recuperação de dados. Você terminará com testes que não lhe dizem nada.


Abordagem muito perspicaz e, como você diz, acho que realmente atingiu uma das principais dúvidas que eu tinha. Obrigado!
precisa saber é o seguinte

0

Um exemplo de caso de falha que você prefere capturar é que o objeto em teste usa uma camada de cache, mas falha em manter os dados conforme necessário. Então, se você consultar o objeto, ele dirá "sim, eu tenho o novo nome e endereço", mas você deseja que o teste falhe porque ele não fez o que deveria.

Como alternativa (e negligenciando a violação de responsabilidade única), suponha que seja necessário persistir uma versão codificada em UTF-8 da sequência em um campo orientado a bytes, mas na verdade persiste o Shift JIS. Algum outro componente vai ler o banco de dados e espera ver UTF-8, daí o requisito. Em seguida, a viagem de ida e volta por esse objeto relatará o nome e o endereço corretos, pois o converterá novamente no Shift JIS, mas o erro não foi detectado pelo seu teste. Esperamos que seja detectado por algum teste de integração posterior, mas o objetivo principal dos testes de unidade é detectar problemas mais cedo e saber exatamente qual componente é responsável.

Se um deles não estiver fazendo o que deveria, seu próprio caso de teste falhará e podemos corrigi-lo e executar a bateria de teste novamente.

Você não pode assumir isso, porque se você não for cuidadoso, escreva um conjunto de testes mutuamente dependentes. O "salva?" test chama o método save de teste e, em seguida, o método load para confirmar que ele foi salvo. O "ele carrega?" test chama o método save para configurar o equipamento de teste e, em seguida, o método load está testando para verificar o resultado. Ambos os testes contam com a exatidão do método que não estão testando, o que significa que nenhum deles realmente testa a exatidão do método que está testando.

A pista de que há um problema aqui é que dois testes que supostamente estão testando unidades diferentes realmente fazem a mesma coisa . Ambos chamam um setter seguido por um getter e depois verificam se o resultado é o valor original. Mas você queria testar se o setter persiste nos dados, não que o par setter / getter funcione juntos. Para que você saiba que algo está errado, basta descobrir o que e corrigir os testes.

Se o seu código foi bem projetado para teste de unidade, há pelo menos duas maneiras de testar se os dados foram realmente mantidos corretamente pelo método em teste:

  • zombe da interface do banco de dados e faça com que seu registro zombe do fato de que as funções apropriadas foram chamadas nela, com os valores esperados. Isso testa o método faz o que deveria e é o teste de unidade clássico.

  • transmita a ele um banco de dados real com exatamente a mesma intenção , para registrar se os dados foram ou não mantidos corretamente. Mas, em vez de ter uma função falsa que apenas diz "sim, eu obtive os dados corretos", seu teste é lido diretamente diretamente no banco de dados e confirma que está correto. Este pode não ser o teste mais puro possível, porque um mecanismo de banco de dados inteiro é uma coisa muito importante para escrever uma simulação glorificada, com mais chances de eu ignorar alguma sutileza que faz um teste passar, mesmo que algo esteja errado (por exemplo, eu não deve usar a mesma conexão com o banco de dados para ler como foi usada para escrever, porque talvez eu veja uma transação não confirmada). Mas ele testa a coisa certa e, pelo menos, você sabe que precisamente implementa toda a interface do banco de dados sem precisar escrever nenhum código falso!

Portanto, é um mero detalhe da implementação do teste se eu leio os dados do banco de dados de teste pelo JDBC ou se eu zoo o banco de dados. De qualquer maneira, o ponto é que eu posso testar a unidade melhor isolando-a do que posso, se eu permitir que ela conspire com outros métodos incorretos da mesma classe para parecer correta, mesmo quando algo está errado. Portanto, quero usar qualquer meio conveniente para verificar se os dados corretos foram mantidos, além de confiar no componente cujo método estou testando.

Se o seu código não for bem projetado para teste de unidade, talvez você não tenha escolha, porque o objeto cujo método você deseja testar pode não aceitar o banco de dados como uma dependência injetada. Nesse caso, a discussão sobre a melhor maneira de isolar a unidade em teste muda para uma discussão sobre o quão perto é possível chegar ao isolamento da unidade em teste. A conclusão é a mesma, no entanto. Se você pode evitar conspirações entre unidades defeituosas, o faz, sujeito ao tempo disponível e a qualquer outra coisa que você pense que seria mais eficaz para encontrar falhas no código.


0

É assim que eu gosto de fazer. Esta é apenas uma escolha pessoal, pois isso não afetará o resultado do produto, apenas a maneira de produzi-lo.

Isso depende das possibilidades de poder adicionar valores simulados no seu banco de dados sem o aplicativo que você está testando.

Digamos que você tenha dois testes: A - lendo o banco de dados B - insira no banco de dados (depende de A)

Se A for aprovado, você tem certeza de que o resultado de B dependerá da parte testada em B e não das dependências. Se A falhar, você poderá ter um falso negativo para B. O erro pode estar em B ou em A. Você não pode ter certeza até A retornar um sucesso.

Eu não tentaria escrever algumas consultas em um teste de unidade, pois você não deveria saber a estrutura do banco de dados por trás do aplicativo (ou isso pode mudar). Significando que seu caso de teste pode falhar devido ao seu próprio código. A menos que você escreva um teste para o teste, mas quem testará o teste para o teste ...


-1

No final, tudo se resume ao que você deseja testar.

Às vezes, basta testar a interface fornecida da sua classe (por exemplo, o registro é criado e armazenado e pode ser recuperado mais tarde, talvez após uma "reinicialização"). Nesse caso, é bom (e geralmente uma boa idéia) usar os métodos fornecidos para o teste. Isso permite que os testes sejam reutilizados, por exemplo, se você deseja alterar o mecanismo de armazenamento (banco de dados diferente, banco de dados na memória para teste, ...).

Observe que isso significa que você realmente se importa com o fato de a classe estar cumprindo seu contrato (criando, armazenando e recuperando registros neste caso), não como ela consegue isso. Se você criar seus testes de unidade de maneira inteligente (em uma interface, não diretamente em uma classe), poderá testar diferentes implementações, pois elas também precisam cumprir o contrato da interface.

Por outro lado, em alguns casos, é necessário verificar como as coisas persistem (talvez algum outro processo dependa dos dados no formato correto nesse banco de dados?). Agora você não pode confiar apenas em recuperar o registro correto da classe; em vez disso, é necessário verificar se ele está armazenado corretamente no banco de dados. E a maneira mais direta de fazer isso é consultar o próprio banco de dados.

Agora você não se importa apenas com a classe que adere ao contrato (criar, armazenar e recuperar), mas também com a forma como ela consegue isso (já que os dados persistentes precisam aderir a outra interface). No entanto, agora os testes dependem da interface da classe e do formato de armazenamento; portanto, será difícil reutilizá-los completamente. (Alguns podem chamar esses tipos de testes de "testes de integração".)


@ downvoters: você pode me dizer suas razões para que eu possa melhorar os aspectos que você acha que minha resposta está faltando?
Hoffmale # /
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.