Como os testes de unidade facilitam o design?

43

Nosso colega promove testes de unidade de escrita como realmente nos ajudando a refinar nosso design e refatorar coisas, mas não vejo como. Se eu estiver carregando um arquivo CSV e analisá-lo, como o teste de unidade (validando os valores nos campos) me ajudará a verificar meu design? Ele mencionou acoplamento e modularidade, etc., mas para mim isso não faz muito sentido - mas eu não tenho muita base teórica.

Isso não é o mesmo que a pergunta que você marcou como duplicada, eu estaria interessado em exemplos reais de como isso ajuda, não apenas na teoria dizendo "isso ajuda". Gosto da resposta abaixo e do comentário, mas gostaria de saber mais.

Usuário039402
fonte
7
Possível duplicata de TDD leva ao bom design?
gnat
3
A resposta abaixo é realmente tudo o que você precisa saber. Sentado ao lado das pessoas que escrevem fábricas de fábrica injetadas por dependência com raiz agregada o dia inteiro, está um cara que escreve discretamente código simples em testes de unidade que funcionam corretamente, é fácil de verificar e já está documentado.
Robert Harvey
4
@gnat fazendo testes de unidade não implica automaticamente TDD, é uma questão diferente
Joppe
11
"teste de unidade (validando os valores nos campos)" - você parece estar unindo testes de unidade com validação de entrada.
9137 jonrsharpe
1
@jonrsharpe Como o código está analisando um arquivo CSV, ele pode estar falando de um teste de unidade real que verifica se uma determinada string CSV fornece a saída esperada.
jollyjoker

Respostas:

3

Os testes de unidade não apenas facilitam o design, mas esse é um dos principais benefícios.

A escrita do primeiro teste elimina a modularidade e a estrutura de código limpa.

Quando você escreve seu código primeiro, você descobrirá que quaisquer "condições" de uma determinada unidade de código são naturalmente enviadas para dependências (geralmente através de zombarias ou stubs) quando você as assume em seu código.

"Com a condição x, esperar o comportamento y", muitas vezes se tornará um esboço de fornecimento x(que é um cenário no qual o teste precisa verificar o comportamento do componente atual) e yse tornará uma farsa, uma chamada à qual será verificada em o final do teste (a menos que seja um "deve retornar y", nesse caso, o teste apenas verificará o valor de retorno explicitamente).

Então, depois que esta unidade se comportar conforme especificado, você passa a escrever as dependências (para xe y) que você descobriu.

Isso torna a escrita de código modular e limpo um processo muito fácil e natural, onde, caso contrário, é fácil desfocar responsabilidades e unir comportamentos sem perceber.

Escrever testes posteriormente informará quando seu código está mal estruturado.

Quando escrever testes para um pedaço de código se torna difícil porque há muitas coisas para stub ou zombar, ou porque as coisas estão muito juntas, você sabe que tem melhorias a fazer no seu código.

Quando "alterar testes" se torna um fardo, porque há muitos comportamentos em uma única unidade, você sabe que possui melhorias a serem feitas no seu código (ou simplesmente na sua abordagem para escrever testes - mas esse não é geralmente o caso na minha experiência) .

Quando seus cenários se tornam muito complicados ("se xe ye zdepois ...") porque você precisa abstrair mais, você sabe que possui melhorias a serem feitas no seu código.

Quando você termina os mesmos testes em dois equipamentos diferentes devido à duplicação e redundância, sabe que possui melhorias a serem feitas no seu código.

Aqui está uma excelente palestra de Michael Feathers demonstrando a estreita relação entre testabilidade e design no código (originalmente publicado por displayName nos comentários). A palestra também aborda algumas queixas e conceitos errôneos sobre bom design e testabilidade em geral.

Formiga P
fonte
@SSECommunity: Com apenas 2 votos a partir de hoje, é muito fácil ignorar essa resposta. Eu recomendo o Talk by Michael Feathers que foi vinculado nesta resposta.
displayName
103

O melhor dos testes de unidade é que eles permitem que você use seu código como outros programadores o usarão.

Se o seu código for desajeitado para teste de unidade, provavelmente será difícil de usar. Se você não pode injetar dependências sem pular obstáculos, seu código provavelmente será inflexível de usar. E se você precisar gastar muito tempo configurando dados ou descobrindo em que ordem fazer as coisas, seu código em teste provavelmente tem muito acoplamento e será difícil trabalhar com isso.

Telastyn
fonte
7
Ótima resposta. Eu sempre gosto de pensar nos meus testes como o primeiro cliente do código; se for doloroso escrever os testes, será doloroso escrever código que consome a API ou o que quer que esteja desenvolvendo.
Stephen Byrne
41
Na minha experiência, a maioria dos testes de unidade " não usa seu código como outros programadores usarão seu código". Eles usam seu código como testes de unidade usarão o código. É verdade que eles revelarão muitas falhas sérias. Mas uma API projetada para teste de unidade pode não ser a API mais adequada para uso geral. Testes de unidade escritos de maneira simplista geralmente exigem que o código subjacente exponha muitos internos. Mais uma vez, com base na minha experiência - estaria interessado em saber como você lidou com isso. (Veja a minha resposta abaixo) #
user949300
7
@ user949300 - Não acredito muito no teste primeiro. Minha resposta é baseada na idéia do código (e certamente do design) primeiro. As APIs não devem ser projetadas para teste de unidade, elas devem ser projetadas para o seu cliente. Os testes de unidade ajudam a aproximar seu cliente, mas são uma ferramenta. Eles estão lá para servi-lo, não vice-versa. E eles certamente não vão impedir você de criar códigos ruins.
Telastyn
3
O maior problema com testes de unidade na minha experiência é que escrever bons é tão difícil quanto escrever um bom código em primeiro lugar. Se você não conseguir distinguir um código bom de um código incorreto, escrever testes de unidade não melhorará seu código. Ao escrever o teste de unidade, você deve ser capaz de distinguir entre o que é um uso suave e agradável e um uso "estranho" ou difícil. Eles podem fazer você usar seu código um pouco, mas não o forçam a reconhecer que o que você está fazendo é ruim.
Jpmc26
2
@ user949300 - o exemplo clássico que eu tinha em mente aqui é um Repositório que precisa de um connString. Suponha que você a exponha como uma propriedade pública gravável e precise defini-la depois de criar um Repositório (). A idéia é que, depois da 5ª ou 6ª vez, você tenha escrito um teste que se esquece de dar esse passo - e, portanto, travar -, você estará "naturalmente" inclinado a forçar o connString a ser uma classe invariável - passado no construtor - tornando sua API melhor e aumentando a probabilidade de gravação de código de produção que evita essa armadilha. Não é uma garantia, mas ajuda, imo.
Stephen Byrne
31

Demorei um pouco para perceber, mas o benefício real (para mim, sua milhagem pode variar) de se fazer desenvolvimento orientado a testes ( usando testes de unidade) é que você precisa fazer o design da API antecipadamente !

Uma abordagem típica do desenvolvimento é primeiro descobrir como resolver um determinado problema e, com esse conhecimento e projeto de implementação inicial, de alguma forma, para invocar sua solução. Isso pode dar alguns resultados bastante interessantes.

Ao fazer o TDD, você deve escrever primeiro o código que usará sua solução. Parâmetros de entrada e saída esperada para garantir que esteja correto. Isso, por sua vez, exige que você descubra o que realmente precisa para fazer, para poder criar testes significativos. Então, e somente então, você implementa a solução. Também é minha experiência que, quando você sabe exatamente o que seu código deve alcançar, ele fica mais claro.

Depois, os testes de unidade após a implementação ajudam a garantir que a refatoração não interrompa a funcionalidade e fornecem documentação sobre como usar seu código (que você sabe que está correto quando o teste foi aprovado!). Mas estes são secundários - o maior benefício é a mentalidade de criar o código em primeiro lugar.

Thorbjørn Ravn Andersen
fonte
Isso é certamente um benefício, mas não acho que seja o benefício "real" - o benefício real vem do fato de que escrever testes para o seu código leva naturalmente "condições" a dependências e anula a injeção excessiva de dependências (promovendo ainda mais a abstração ) antes de começar.
Ant P
O problema é que você escreve todo um conjunto de testes antecipadamente que correspondem a essa API; então, ele não funciona exatamente conforme necessário e você deve reescrever seu código e todos os testes. Para APIs voltadas ao público, é provável que elas não mudem e essa abordagem é adequada. No entanto, as APIs de código que só é usado internamente muda muito como você descobrir como implementar um recurso que precisa de um monte de APIs semi-privados trabalhando juntos
Juan Mendes
@ AntP Sim, isso faz parte do design da API.
Thorbjørn Ravn Andersen
@JuanMendes Isso não é incomum e esses testes precisam ser alterados, assim como qualquer outro código quando você altera os requisitos. Um bom IDE irá ajudá-lo a refazer as classes como parte do trabalho a ser feito automaticamente quando você alterar o método de assinaturas etc.
Thorbjørn Ravn Andersen
@JuanMendes Se você está escrevendo bons testes e pequenas unidades, o impacto do efeito que você está descrevendo é pequeno ou nenhum na prática.
Ant P
6

Eu concordo 100% de que os testes de unidade nos ajudam "a refinar nosso design e refatorar as coisas".

Penso se eles o ajudarão a fazer o design inicial . Sim, eles revelam falhas óbvias e forçam você a pensar em "como posso tornar o código testável"? Isso deve levar a menos efeitos colaterais, configurações e configurações mais fáceis, etc.

No entanto, na minha experiência, testes de unidade excessivamente simplistas, escritos antes que você realmente entenda qual deve ser o design (é certo que é um exagero do TDD de núcleo duro, mas os codificadores muitas vezes escrevem um teste antes que pensem muito) geralmente levam à anemia modelos de domínio que expõem muitos internos.

Minha experiência com TDD foi há vários anos, então estou interessado em saber quais técnicas mais recentes podem ajudar na escrita de testes que não influenciam muito o design subjacente. Obrigado.

user949300
fonte
Um longo número de parâmetros de método é um cheiro de código e uma falha de design.
Sufian
5

O teste de unidade permite que você veja como as interfaces entre as funções funcionam e, muitas vezes, fornece informações sobre como melhorar o design local e o design geral. Além disso, se você desenvolver seus testes de unidade enquanto desenvolve seu código, terá um conjunto de testes de regressão pronto. Não importa se você está desenvolvendo uma interface do usuário ou uma biblioteca de back-end.

Depois que o programa é desenvolvido (com testes de unidade), conforme os erros são descobertos, você pode adicionar testes para confirmar que os erros foram corrigidos.

Eu uso o TDD para alguns dos meus projetos. Esforço-me bastante na elaboração de exemplos extraídos de livros didáticos ou de papéis considerados corretos e testo o código que estou desenvolvendo usando esse exemplo. Quaisquer mal-entendidos que tenho sobre os métodos se tornam muito aparentes.

Costumo ser um pouco mais flexível do que alguns de meus colegas, pois não me importo se o código for escrito primeiro ou se o teste for escrito primeiro.

Robert Baron
fonte
Essa é uma ótima resposta para mim. Você se importaria em colocar alguns exemplos, por exemplo, um para cada caso (quando você obtiver informações sobre o design etc.).
usar o seguinte comando
5

Quando você deseja testar o seu analisador de unidade, detectando corretamente a delimitação de valor, pode passar uma linha a partir de um arquivo CSV. Para tornar seu teste direto e curto, você pode testá-lo através de um método que aceita uma linha.

Isso fará com que você separe automaticamente a leitura de linhas da leitura de valores individuais.

Em outro nível, talvez você não queira colocar todos os tipos de arquivos CSV físicos em seu projeto de teste, mas faça algo mais legível, apenas declarando uma grande cadeia de CSV dentro do seu teste para melhorar a legibilidade e a intenção do teste. Isso levará você a desacoplar seu analisador de qualquer E / S que você faria em outro lugar.

Apenas um exemplo básico, comece a praticar, você sentirá a magia em algum momento (eu tenho).

Joppe
fonte
4

Simplificando, escrever testes de unidade ajuda a expor falhas no seu código.

Este guia espetacular para escrever código testável , escrito por Jonathan Wolter, Russ Ruffer e Miško Hevery, contém vários exemplos de como as falhas no código que inibem o teste também impedem a reutilização fácil e a flexibilidade do mesmo código. Portanto, se seu código é testável, é mais fácil de usar. A maioria dos "costumes" são dicas ridiculamente simples que aprimoram bastante o design do código ( Dependency Injection FTW).

Por exemplo: É muito difícil testar se o método computeStuff funciona corretamente quando o cache começa a despejar coisas. Isso ocorre porque você deve adicionar porcaria manualmente ao cache até que o "bigCache" esteja quase cheio.

public OopsIHardcoded {

   Cache cacheOfExpensiveComputations;

   OopsIHardcoded() {
       this.cacheOfExpensiveComputation = buildBigCache();
   }

   ExpensiveValue computeStuff() {
      //DOES THIS WORK CORRECTLY WHEN CACHE EVICTS DATA?
   }
}

No entanto, quando usamos injeção de dependência, é muito mais fácil testar se o método computeStuff funciona corretamente quando o cache começa a despejar coisas. Tudo o que fazemos é criar um teste em que chamamos de new HereIUseDI(buildSmallCache()); Aviso, temos um controle mais matizado do objeto e ele paga dividendos imediatamente.

public HereIUseDI {

   Cache cacheOfExpensiveComputations;

   HereIUseDI(Cache cache) {
       this.cacheOfExpensiveComputation = cache;
   }

   ExpensiveValue computeStuff() {
      //DOES THIS WORK CORRECTLY WHEN CACHE EVICTS DATA?
   }
}

Benefícios semelhantes podem ser obtidos quando nosso código exige dados que geralmente são mantidos em um banco de dados ... basta transmitir EXATAMENTE os dados necessários.

Ivan
fonte
2
Honestamente, não tenho certeza de como você entende o exemplo. Como o método computeStuff se relaciona com o cache?
João V
1
@ user970696 - Sim, estou sugerindo que "computeStuff ()" usa o cache. A pergunta é "computeStuff () funciona corretamente o tempo todo (que depende do estado do cache)". Consequentemente, é difícil confirmar que computeStuff () faça o que você deseja PARA TODOS OS POSSÍVEIS ESTADOS DO CACHE, se você não puder definir diretamente / crie o cache porque você codificou a linha "cacheOfExpensiveComputation = buildBigCache ();" (em oposição a passagem no cache directamente através do construtor)
Ivan
0

Dependendo do significado de 'testes de unidade', não acho que os testes de unidade de nível realmente baixo facilitem o bom design, tanto quanto os testes de integração de nível um pouco mais alto - testes que testam um grupo de atores (classes, funções, o que for) seu código combina adequadamente para produzir um monte de comportamentos desejáveis ​​que foram acordados entre a equipe de desenvolvimento e o proprietário do produto.

Se você pode escrever testes nesses níveis, isso leva você a criar um código legal, lógico e parecido com a API que não requer muitas dependências malucas - o desejo de ter uma configuração de teste simples naturalmente o levará a não ter muitas dependências malucas ou código fortemente associado.

Não se engane: os testes de unidade podem levar a um design ruim, bem como a um bom design. Vi os desenvolvedores pegar um pouco de código que já possui um bom design lógico e uma única preocupação, separá-lo e introduzir mais interfaces apenas para fins de teste e, como resultado, tornar o código menos legível e mais difícil de mudar , além de possivelmente ter mais bugs, se o desenvolvedor decidir que ter muitos testes de unidade de baixo nível significa que eles não precisam ter testes de nível superior. Um exemplo favorito em particular é um bug que eu consertei onde havia muitos códigos 'testáveis' e muito detalhados, relacionados à obtenção de informações dentro e fora da área de transferência. Tudo dividido e dissociado em níveis muito pequenos de detalhes, com muitas interfaces, muitas zombarias nos testes e outras coisas divertidas. Apenas um problema: não havia nenhum código que realmente interagisse com o mecanismo da área de transferência do sistema operacional,

Os testes de unidade podem definitivamente orientar o seu design - mas eles não o guiam automaticamente para um bom design. Você precisa ter idéias sobre o que é um bom design que vai além de apenas 'esse código é testado, portanto, é testável e, portanto, é bom'.

É claro que se você é uma daquelas pessoas para quem 'testes de unidade' significa 'qualquer teste automatizado que não seja conduzido pela interface do usuário', alguns desses avisos podem não ser tão relevantes - como eu disse, acho que os mais altos Os testes de integração de nível de nível geralmente são os mais úteis quando se trata de direcionar seu design.

topo Restabelecer Monica
fonte
-2

Os testes de unidade podem ajudar na refatoração quando o novo código passa em todos os testes antigos .

Digamos que você implementou uma classificação de bolhas porque estava com pressa e não estava preocupado com o desempenho, mas agora deseja uma classificação rápida, porque os dados estão ficando mais longos. Se todos os testes forem aprovados, as coisas parecem boas.

É claro que os testes precisam ser abrangentes para fazer isso funcionar. No meu exemplo, seus testes podem não abranger a estabilidade, porque isso não preocupava o bubblesort.

om
fonte
1
Isso é verdade, mas é mais um benefício de manutenção do que um impacto direto na qualidade do design do código.
Ant P
@ AntP, o OP perguntou sobre refatoração e testes de unidade.
om
1
A pergunta mencionava refatoração, mas a pergunta real era sobre como os testes de unidade poderiam melhorar / verificar o design do código - não facilitar o processo de refatoração em si.
Ant P
-3

Eu descobri que os testes de unidade são mais valiosos para facilitar a manutenção a longo prazo de um projeto. Quando volto a um projeto depois de meses e não me lembro de muitos detalhes, a execução de testes me impede de quebrar as coisas.

David Baker
fonte
6
Esse certamente é um aspecto importante dos testes, mas não responde realmente à pergunta (não é por isso que os testes são bons, mas como eles influenciam o design).
casco