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

80

Hoje assisti a um vídeo " JUnit basics" e o autor disse que, ao testar um determinado método no seu programa , você não deve usar outros métodos próprios no processo.

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

Acho que entendo o espírito de sua afirmação: você não quer 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 é feito escrevendo (de novo) seu próprio código de validação / 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. Quer dizer, se executarmos toda a bateria de teste e testarmos todos os nossos métodos públicos completamente (incluindo o método DAO neste caso) ), não deveria estar certo usar apenas alguns desses métodos enquanto testava outros métodos? Se um deles não está fazendo o que é suposto, então seu próprio caso de teste falhará, e nós podemos consertá-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 strong sentimento sobre isso por causa de vários Excel recentes - VBA que escrevi (testados em unidades adequadamente graças ao Rubberduck for VBA ), onde aplicar esta recomendação significaria muito trabalho extra adicional, sem benefício aparente.

Você pode compartilhar suas ideias sobre isso?

    
por carlossierra 06.09.2016 / 17:40
fonte

11 respostas

186

O espírito de sua afirmação é de fato correto. O ponto dos testes unitários é isolar o código, testá-lo livre de dependências, para que qualquer comportamento errôneo possa ser rapidamente reconhecido onde está acontecendo.

Com isso dito, o teste de unidade é uma ferramenta, e destina-se a servir aos seus propósitos, não é um altar para o qual se deve orar. Às vezes isso significa deixar as dependências porque elas funcionam de forma confiável o suficiente e você não quer se incomodar em zombar delas, às vezes isso significa que alguns dos seus testes de unidade são realmente muito próximos, se não testes de integração.

Em última análise, você não está sendo avaliado, o que é importante é o produto final do software que está sendo testado, mas você só precisa ficar atento a quando está distorcendo as regras e decidindo quando as compensações são vale a pena.

    
por 06.09.2016 / 18:55
fonte
36

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

Para outros, 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 eu puder usar esse termo) pode chamar isso de teste de integração , diferenciado de um teste de unidade com base em que o teste depende de mais de uma unidade pura de código.

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

Usando essas definições, você não deve depender de código fora da unidade sob teste em um "teste unitário". Em um "teste de integração", no entanto, é perfeitamente razoável escrever um teste que exerça alguma parte específica do código e inspecione o banco de dados para os critérios de aprovação.

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

    
por 06.09.2016 / 19:13
fonte
7

A resposta é sim e não ...

Testes exclusivos que funcionam isoladamente são uma necessidade absoluta, pois permitem que você estabeleça o trabalho básico de que seu código de baixo nível está funcionando adequadamente. Mas na codificação de bibliotecas maiores, você também encontrará áreas de código que exigirão testes com unidades cruzadas.

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

  • Sem um teste isolado para confirmar o que está quebrando, um teste de "unidade cruzada" com falha exigirá uma solução de problemas adicional para determinar o que há de errado com seu código
  • Confiar demais em testes de unidade cruzada pode tirar você da mentalidade de contrato em que você deve estar sempre ao escrever o código SOLID Object-Oriented. Testes isolados normalmente fazem sentido porque suas unidades devem estar executando 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 não deseja que ocorra em um ambiente de produção. Esta é uma das muitas razões pelas quais zombar de frameworks como Mockito são tão populares porque permite que você falsifique um objeto e imite um teste final sem terminar mudando algo que você não deveria.

No final do dia, você quer 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 no motivo pelo qual você está escrevendo testes em primeiro lugar. Eles estão lá para lhe dar confiança de que seu código está funcionando da maneira esperada. Tudo o que você precisa fazer para que isso aconteça é bom.

O fato triste, mas verdadeiro, é que se você estiver usando testes automatizados, então você já tem uma vantagem sobre muitos desenvolvedores. Boas práticas de teste são a primeira coisa a se deslizar quando uma equipe de desenvolvimento enfrenta prazos difíceis. Portanto, contanto que você esteja aderindo às suas armas e escrevendo testes unitários que sejam um reflexo significativo de como seu código deve funcionar, eu não ficaria obcecado em quão "puros" são seus testes.

    
por 06.09.2016 / 19:01
fonte
4

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

Reduza suas unidades, divida sua turma, refatorie e redesenhe até que seja mais fácil testar sem reimplementar o resto do seu objeto. Obtenha alguma ajuda se achar que seu código ou testes não podem ser mais simplificados. Tente pensar em um colega que parece sempre ter sorte e ter tarefas fáceis de testar. Peça a ele ou ela ajuda, porque não é sorte.

    
por 06.09.2016 / 21:10
fonte
1

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

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

Você pode querer atalho o teste para melhorar o desempenho usando um banco de dados simulado, mas tive contratados que fizeram exatamente isso e saíram no final do contrato com um sistema que passou em tais testes, mas não armazenou nada de fato no banco de dados entre reinicializações do sistema.

Você pode argumentar se 'unidade' no teste unitário indica uma 'unidade de código' ou 'unidade de funcionalidade', funcionalidade talvez criada por muitas unidades de código em conjunto. Não acho isso uma distinção útil - as perguntas que tenho em mente são: "o teste lhe 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á TDD no sistema - você ainda não escreveu o 'obter objeto do registro do banco de dados' ainda assim não pode testar a unidade completa de funcionalidade - mas são frágeis contra mudanças de implementação remova-os assim que a operação completa for testável.

    
por 07.09.2016 / 10:14
fonte
1

O espírito está correto.

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

O ideal seria eliminar todo o sistema de banco de dados. Ou seja, você executaria seu método em um ambiente falsificado e apenas garantiria que ele chamasse as APIs de banco de dados corretas na ordem correta. Você explicitamente, positivamente, não quer testar o banco de dados ao testar um de seus próprios métodos.

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

Naturalmente, 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 unit .

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

    
por 07.09.2016 / 11:53
fonte
1

Há pelo menos uma razão muito importante não para usar o acesso direto ao banco de dados em testes de unidade para código comercial que interage com o banco de dados: se você alterar a implementação do banco de dados, precisará reescrever todos esses testes unitários. Renomeie uma coluna, para o seu código, basta alterar uma única linha na definição do mapeador de dados. Se você não usar seu mapeador de dados durante o teste, no entanto, você descobrirá que também precisa alterar cada teste unitário que faz referência a essa coluna . Isso pode se transformar em uma quantidade extraordinária de trabalho, especialmente para mudanças mais complexas que são menos passíveis de pesquisa e substituição.

Além disso, usar seu mapeador de dados como um mecanismo abstrato em vez de conversar diretamente com o banco de dados torna mais fácil remover completamente a dependência do banco de dados , que pode não ser relevante agora, mas Até milhares de testes de unidade e todos eles estão atingindo um banco de dados, você ficará agradecido por poder refatorá-los facilmente para remover essa dependência, pois o seu conjunto de testes diminuirá de alguns minutos para em segundos, o que pode ter um enorme benefício em sua produtividade.

Sua pergunta indica que você está pensando no caminho certo. Como você suspeita, os testes de unidade também são códigos. É tão importante torná-los passíveis de manutenção e fácil de se adaptar a mudanças futuras quanto ao resto da sua base de código. Esforce-se para mantê-los legíveis e eliminar a duplicação, e você terá uma suíte de testes muito melhor para isso. E uma boa suíte de testes é aquela que é usada. E um conjunto de testes que é usado ajuda a encontrar erros, enquanto um que não é usado é inútil.

    
por 08.09.2016 / 19:11
fonte
1

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

Quando eu olho dessa maneira, um método que persiste dados e outro método que 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? Ela persiste widgets. Ok, como você verifica se persiste widgets? Bem, qual é a definição de persistência? Isso significa que quando você escreve, você pode ler de volta. Então leia + escreva junto torne-se 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 de opinião, significativo.

As outras respostas enfocam o quão importante é o isolamento, o debate pragmático versus o realista, e se um teste de unidade pode ou não consultar um banco de dados. Aqueles realmente não respondem a pergunta embora.

Para testar se algo pode ser armazenado, você precisa armazená-lo e depois lê-lo de volta. Você não pode testar se algo está armazenado se você não tem permissão para lê-lo. Não teste o armazenamento de dados separadamente da recuperação de dados. Você vai acabar com testes que não dizem nada.

    
por 08.09.2016 / 22:54
fonte
0

Um exemplo de um caso de falha que você prefiraria capturar é que o objeto em teste usa uma camada de armazenamento em cache, mas não mantém os dados conforme necessário. Então, se você consultar o objeto, ele vai dizer "yup, eu tenho o novo nome e endereço", mas você quer que o teste falhe, porque ele não fez realmente o que deveria.

Como alternativa (e com vista para a violação de responsabilidade única), suponha que seja necessário persistir uma versão codificada em UTF-8 da string para um campo orientado a byte, mas na verdade persiste o Shift JIS. Algum outro componente vai ler o banco de dados e espera ver o UTF-8, daí o requisito. Em seguida, a ida e volta através desse objeto informará o nome e o endereço corretos, pois ele será convertido de volta do Shift JIS, mas o erro não será detectado pelo seu teste. Espera-se que seja detectado por algum teste de integração posterior, mas o objetivo dos testes unitários é detectar os problemas precocemente e saber exatamente qual componente é responsável.

If one of them is not doing what it's supposed to, then its own test case will fail and we can fix it and run the test battery again.

Você não pode assumir isso, porque se você não for cuidadoso, escreva um conjunto de testes mutuamente dependente. O "salva?" teste chama o método save que está testando e, em seguida, o método load para confirmar que ele foi salvo. O "carrega?" teste chama o método save para configurar o equipamento de teste e, em seguida, o método de carga que está testando para verificar o resultado. Ambos os testes dependem da exatidão do método que eles 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 diferentes unidades realmente fazem a mesma coisa . Ambos chamam um setter seguido por um getter, depois checam se o resultado é o valor original. Mas você queria testar se o setter persiste os dados, não que o par setter / getter trabalhe em conjunto. Então você sabe que algo está errado, você só tem que descobrir o que e consertar os testes.

Se o seu código for bem projetado para testes unitários, haverá pelo menos duas maneiras de testar se os dados foram realmente persistidos corretamente pelo método em teste:

  • zombar da interface do banco de dados, e ter seu registro simulado o fato de que as funções apropriadas foram chamadas, com os valores esperados. Isso testa o método faz o que é suposto e é o teste unitário clássico.

  • passe um banco de dados real com exatamente a mesma intenção , para registrar se os dados foram ou não corretamente persistidos. Mas, em vez de ter uma função zombada que diz apenas "sim, recebi os dados corretos", o teste é lido diretamente do banco de dados e confirma que está correto. Este pode não ser o mais mais puro teste possível, porque um mecanismo de banco de dados inteiro é uma grande coisa para usar para escrever um mock glorificado, com mais chances de eu ignorar alguma sutileza que faz um teste passar mesmo que algo está errado (por exemplo, eu não deveria usar a mesma conexão de banco de dados para ler como foi usado para gravar, porque eu posso ver uma transação não confirmada). Mas testa a coisa certa, e pelo menos você sabe que ela precisamente implementa toda a interface do banco de dados sem ter que escrever qualquer código falso!

Por isso, é um mero detalhe de implementação de teste se eu leio os dados do banco de dados de teste pelo JDBC ou se eu zombar do 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 na mesma classe para parecer correta mesmo quando algo está errado. Portanto, eu quero usar qualquer meio conveniente para verificar se os dados corretos foram mantidos, diferente de confiar no componente cujo método eu estou testando.

Se o seu código não for bem projetado para testes unitários, talvez você não tenha escolha, pois 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 próximo é possível isolar a unidade em teste. A conclusão é a mesma, no entanto. Se você puder evitar conspirações entre unidades defeituosas, você o faz, sujeito ao tempo disponível e a qualquer outra coisa que você pense que seria mais eficaz em encontrar falhas no código.

    
por 07.09.2016 / 16:17
fonte
0

É assim que 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 falsos em 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 passar, então você tem certeza de que o resultado de B dependeria da parte testada em B e não das dependências. Se A falhar, você pode ter um falso negativo para B. O erro pode estar em B ou em A. Você não pode saber com certeza até que Um retorno seja um sucesso.

Eu não tentaria escrever algumas consultas em um teste unitário, já que você não deveria saber a estrutura do banco de dados por trás do aplicativo (ou pode mudar). O que significa que o 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 ...

    
por 08.09.2016 / 14:17
fonte
-1

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

Às vezes, é suficiente testar a interface fornecida da sua turma (por exemplo, os registros são criados e armazenados e podem ser recuperados posteriormente, talvez depois de uma "reinicialização"). Neste caso, é bom (e geralmente uma boa idéia) usar os métodos fornecidos para teste. Isso permite que os testes sejam reutilizados, por exemplo, se você quiser alterar o mecanismo de armazenamento (banco de dados diferente, banco de dados na memória para teste, ...).

Note that this means you only really care that the class is adhering to its contract (creating, storing and retrieving records in this case), not how it achieves this. If you create your unit tests smartly (against an interface, not a class directly), you can test different implementations, as they also have to adhere to the contract of the interface.

Por outro lado, em alguns casos, você tem que realmente verificar como o material é persistido (talvez algum outro processo dependa de que os dados estejam no formato correto nesse banco de dados?). Agora você não pode confiar apenas em obter o registro correto de volta da classe, em vez disso, você precisa verificar se está armazenado corretamente no banco de dados. E a maneira mais direta de fazer isso é consultar o próprio banco de dados.

Now you don't only care about the class adhering to its contract (create, store and retrieve), but also how it achieves this (since that persisted data needs to adhere to another interface). However, now the tests are dependent on both the class interface and the storage format, so it will be hard to reuse them fully. (Some might call these kinds of tests "integration tests".)

    
por 06.09.2016 / 23:23
fonte