O teste de unidade leva à generalização prematura (especificamente no contexto de C ++)?

20

Notas preliminares

Não vou entrar na distinção dos diferentes tipos de teste que existem, existem algumas perguntas nesses sites sobre isso.

Vou pegar o que está lá e diz: teste de unidade no sentido de "testar a menor unidade isolável de um aplicativo" da qual essa pergunta realmente deriva

O problema de isolamento

Qual é a menor unidade isolável de um programa. Bem, a meu ver, depende (altamente?) Do idioma em que você está codificando.

Micheal Feathers fala sobre o conceito de costura : [WEwLC, p31]

Uma costura é um local onde você pode alterar o comportamento do seu programa sem editar nesse local.

E sem entrar em detalhes, entendo que uma costura - no contexto de teste de unidade - é um local em um programa em que seu "teste" pode interagir com sua "unidade".

Exemplos

O teste de unidade - especialmente em C ++ - exige que o código em teste adicione mais costuras que seriam estritamente necessárias para um determinado problema.

Exemplo:

  • Adicionando uma interface virtual onde a implementação não virtual seria suficiente
  • Divisão - generalizando (?) - uma classe (pequena) ainda "apenas" para facilitar a adição de um teste.
  • Dividir um projeto executável em bibliotecas aparentemente "independentes", "apenas" para facilitar a compilação independente para os testes.

A questão

Vou tentar algumas versões que esperamos perguntar sobre o mesmo ponto:

  • É a maneira como os testes de unidade exigem que a estrutura "apenas" do código de um aplicativo seja benéfica para os testes de unidade ou é realmente benéfica para a estrutura de aplicativos.
  • A generalização de código necessária para torná-lo testável por unidade é útil para qualquer coisa, exceto os testes de unidade?
  • A adição de testes de unidade força uma generalização desnecessária?
  • Os testes de unidade de forma à força no código "sempre" também são uma boa forma para o código em geral, como visto no domínio do problema?

Lembro-me de uma regra geral que disse não generalizar até que você precise / até que haja um segundo lugar que use o código. Nos testes de unidade, sempre existe um segundo lugar que usa o código - o teste de unidade. Então, esse motivo é suficiente para generalizar?

Martin Ba
fonte
8
Um meme comum é que qualquer padrão pode ser usado em excesso para se tornar um antipadrão. O mesmo acontece com o TDD. Pode-se adicionar interfaces testáveis ​​além do ponto de retornos decrescentes, onde o código testado é menor que as interfaces de teste generalizadas adicionadas, bem como na área de custo-benefício muito baixa. Um jogo casual com interfaces adicionadas para testes como um sistema operacional de missão espacial profunda pode perder completamente sua janela de mercado. Verifique se o teste adicionado está antes desses pontos de inflexão.
hotpaw2
@ hotpaw2 Blasfêmia! :)
maple_shaft

Respostas:

23

O teste de unidade - especialmente em C ++ - exige que o código em teste adicione mais costuras que seriam estritamente necessárias para um determinado problema.

Somente se você não considerar testar uma parte integrante da solução de problemas. Para qualquer problema não trivial, deveria ser, não apenas no mundo do software.

No mundo do hardware, isso foi aprendido há muito tempo - da maneira mais difícil. Os fabricantes de vários equipamentos aprenderam ao longo de séculos, com inúmeras pontes caindo, carros explodindo, CPUs fumando etc. etc. o que estamos aprendendo agora no mundo do software. Todos eles criam "costuras extras" em seus produtos para torná-los testáveis. Atualmente, a maioria dos carros novos possui portas de diagnóstico para os reparadores obterem dados sobre o que está acontecendo dentro do motor. Uma porção significativa dos transistores em cada CPU serve para fins de diagnóstico. No mundo do hardware, todos os custos "extras" de material e, quando um produto é fabricado aos milhões, esses custos certamente somam grandes somas de dinheiro. Ainda assim, os fabricantes estão dispostos a gastar todo esse dinheiro em testes.

De volta ao mundo do software, o C ++ é realmente mais difícil de testar a unidade do que as linguagens posteriores, com carregamento dinâmico de classes, reflexão etc. No projeto C ++ em que usei testes de unidade até o momento, não executamos os testes com a mesma frequência que em um projeto Java, por exemplo - mas eles ainda faziam parte da nossa compilação de IC e os achamos úteis.

A maneira como os testes de unidade exigem que a estrutura do código de um aplicativo seja "apenas" benéfica para os testes de unidade ou é realmente benéfica para a estrutura de aplicativos?

Na minha experiência, um design testável é benéfico em geral, não "apenas" para os próprios testes de unidade. Esses benefícios vêm em diferentes níveis:

  • Tornar seu projeto testável obriga a dividir seu aplicativo em partes pequenas, mais ou menos independentes, que só podem se influenciar de maneiras limitadas e bem definidas - isso é muito importante para a estabilidade e a manutenção do seu programa a longo prazo. Sem isso, o código tende a se deteriorar em código espaguete, onde qualquer alteração feita em qualquer parte da base de código pode causar efeitos inesperados em partes distintas e aparentemente não relacionadas do programa. Escusado será dizer que é o pesadelo de todo programador.
  • Escrever os testes eles mesmos na moda TDD, na verdade, exercita suas APIs, classes e métodos e serve como um teste muito eficaz para detectar se o seu design faz sentido - se a gravação de testes e da interface parece estranha ou difícil, você recebe um feedback antecipado valioso quando é necessário. ainda é fácil modelar a API. Em outras palavras, isso o impede de publicar suas APIs prematuramente.
  • O padrão de desenvolvimento imposto pelo TDD ajuda você a se concentrar nas tarefas concretas a serem executadas e mantém você no alvo, minimizando as chances de você se afastar e resolver outros problemas além do que você deveria, adicionando recursos e complexidade extras desnecessários , etc.
  • O feedback rápido dos testes de unidade permite que você seja ousado na refatoração do código, permitindo adaptar e evoluir constantemente o design ao longo da vida útil do código, impedindo efetivamente a entropia do código.

Lembro-me de uma regra geral que disse não generalizar até que você precise / até que haja um segundo lugar que use o código. Nos testes de unidade, sempre existe um segundo lugar que usa o código - o teste de unidade. Então, esse motivo é suficiente para generalizar?

Se você puder provar que seu software faz exatamente o que deveria fazer - e provar de maneira rápida, repetível, barata e determinística o suficiente para satisfazer seus clientes - sem a generalização "extra" ou costuras forçadas pelos testes de unidade, faça-o (e deixe-nos saber como você faz isso, porque tenho certeza de que muitas pessoas neste fórum estariam tão interessadas quanto eu :-)

Mas suponho que por "generalização" você quer dizer coisas como introduzir uma interface (classe abstrata) e polimorfismo em vez de uma única classe concreta - se não, por favor, esclareça.

Péter Török
fonte
Senhor, eu te saúdo.
21712 GordonGreenM
Uma nota breve, mas pedante: o "porto de diagnóstico" está presente principalmente porque os governos os determinaram como parte de um esquema de controle de emissões. Conseqüentemente, possui severas limitações; existem muitas coisas que poderiam ser diagnosticadas com essa porta que não são (ou seja, algo que não tem a ver com controle de emissões).
Robert Harvey
4

Vou jogar The Way of Testivus em você, mas resumindo:

Se você está gastando muito tempo e energia, tornando seu código mais complicado para testar uma única parte do sistema, pode ser que sua estrutura esteja errada ou que sua abordagem de teste esteja errada.

O guia mais simples é o seguinte: O que você está testando é a interface pública do seu código da maneira como ele deve ser usado por outras partes do sistema.

Se seus testes estão se tornando longos e complicados, é uma indicação de que o uso da interface pública será difícil.

Se você precisar usar a herança para permitir que sua classe seja usada por qualquer coisa que não seja a única instância para a qual ela será usada atualmente, há uma boa chance de sua classe estar muito ligada ao ambiente de uso. Você pode dar um exemplo de uma situação em que isso é verdade?

No entanto, cuidado com o dogma de teste de unidade. Escreva o teste que permite detectar o problema que fará com que o cliente grite com você .

Deworde
fonte
Eu adicionaria o mesmo: faça uma API, teste a API, do lado de fora.
Christopher Mahan
2

TDD e teste de unidade, é bom para o programa como um todo, e não apenas para os testes de unidade. A razão para isso é porque é bom para o cérebro.

Esta é uma apresentação sobre uma estrutura específica do ActionScript chamada RobotLegs. No entanto, se você folhear os 10 primeiros slides, mais ou menos, ele começará a obter as partes boas do cérebro.

O teste de TDD e de unidade obriga você a se comportar de uma maneira que seja melhor para o cérebro processar e lembrar informações. Portanto, enquanto sua tarefa exata à sua frente é apenas fazer um teste de unidade melhor ou tornar o código mais testável por unidade ... o que realmente faz é tornar seu código mais legível e, assim, torná-lo mais sustentável. Isso permite que você codifique habbits mais rapidamente e permite que você compreenda seu código mais rapidamente quando precisar adicionar / remover recursos, corrigir bugs ou, em geral, abrir o arquivo de origem.

Prumo
fonte
1

testando a menor unidade isolável de uma aplicação

isso é verdade, mas se você levar muito longe, não lhe dará muito, e custa muito, e acredito que é esse aspecto que está promovendo o uso do termo BDD para ser o que o TDD deveria ter sido. junto - a menor unidade isolável é o que você quer que seja.

Por exemplo, uma vez depurei uma classe de rede que tinha (entre outros bits) 2 métodos: 1 para definir o endereço IP, outro para definir o número da porta. Naturalmente, esses métodos eram muito simples e passariam no teste mais trivial facilmente, mas se você definir o número da porta e depois o endereço IP, ele não funcionará - o configurador de ip substitui o número da porta por padrão. Então você teve que testar a classe como um todo para garantir o comportamento correto, algo que acho que falta no conceito de TDD, mas o BDD fornece. Você realmente não precisa testar cada método minúsculo, quando pode testar a área mais sensível e menor do aplicativo em geral - nesse caso, a classe de rede.

Em última análise, não existe uma bala mágica para o teste, você precisa tomar decisões sensatas sobre quanto e com que granularidade aplicar seus recursos limitados de teste. A abordagem baseada em ferramentas que gera automaticamente stubs para você não faz isso, é uma abordagem de força contundente.

Portanto, considerando isso, você não precisa estruturar seu código de uma certa maneira para obter o TDD, mas o nível de teste obtido dependerá da estrutura do seu código - se você tiver uma GUI monolítica com toda a sua lógica firmemente ligada a Na estrutura da GUI, será mais difícil isolar essas partes, mas ainda é possível escrever um teste de unidade em que 'unit' se refira à GUI e todo o trabalho de banco de dados de back-end é ridicularizado. Este é um exemplo extremo, mas mostra que você ainda pode fazer testes automatizados.

Um efeito colateral da estruturação do código para facilitar o teste de unidades menores ajuda a definir melhor o aplicativo e permite a substituição mais fácil de peças. Também ajuda na codificação, pois é menos provável que 2 desenvolvedores estejam trabalhando no mesmo componente a qualquer momento - ao contrário de um aplicativo monolítico que possui dependências entrelaçadas que interrompem o trabalho de todos os outros.

gbjbaanb
fonte
0

Você alcançou uma boa percepção sobre compensações no design de idiomas. Algumas das decisões principais de design em C ++ (o mecanismo de função virtual combinado com o mecanismo de chamada de função estática) tornam o TDD difícil. O idioma não suporta realmente o que você precisa para facilitar. É fácil escrever C ++ que é quase impossível para teste de unidade.

Tivemos mais sorte em fazer nosso código TDD C ++ a partir de uma mentalidade quase funcional - funções de gravação e não procedimentos (uma função que não aceita argumentos e retorna nula) e usamos a composição sempre que possível. Como é difícil substituir essas classes membros, nos concentramos em testar essas classes para criar uma base confiável e, em seguida, sabemos que a unidade básica funciona quando a adicionamos a outra coisa.

A chave é a abordagem quase funcional. Pense bem, se todo o seu código C ++ fosse funções gratuitas que não acessassem globals, isso seria muito fácil para o teste de unidade :)

anon
fonte