Tipos de testes de unidade com base na utilidade


13

Do ponto de vista do valor, vejo dois grupos de testes de unidade em minha prática:

  1. Testes que testam alguma lógica não trivial. Escrevê-los (antes da implementação ou depois) revela alguns problemas / possíveis erros e ajuda a ter confiança caso a lógica seja alterada no futuro.
  2. Testes que testam alguma lógica muito trivial. Esses testes são mais parecidos com códigos de documentos (geralmente com zombarias) do que com testes. O fluxo de trabalho de manutenção desses testes não é "alguma lógica alterada, teste ficou vermelho - graças a Deus eu escrevi esse teste", mas "algum código trivial mudou, teste tornou-se falso negativo - eu tenho que manter (reescrever) o teste sem obter lucro" . Na maioria das vezes, esses testes não valem a pena ser mantidos (exceto razões religiosas). E, de acordo com minha experiência em muitos sistemas, esses testes são como 80% de todos os testes.

Estou tentando descobrir o que os outros caras pensam sobre o tema dos testes de unidade de separação por valor e como isso corresponde à minha separação. Mas o que eu mais vejo é propaganda TDD em tempo integral ou propaganda de testes são inúteis, basta escrever o código. Estou interessado em algo no meio. Seus próprios pensamentos ou referências a artigos / papéis / livros são bem-vindos.


3
Eu mantenho testes de unidade verificando erros conhecidos (específicos) - que uma vez escaparam do conjunto de testes de unidade original - como um grupo separado, cuja função é evitar erros de regressão.
Konrad Morawski

6
Esse segundo tipo de teste é o que eu vejo como uma espécie de "fricção de mudança". Não descarte sua utilidade. Alterar até mesmo as trivialidades do código tende a ter efeitos negativos em toda a base de código e a introdução desse tipo de atrito atua como um obstáculo para os desenvolvedores, para que eles apenas alterem as coisas que realmente precisam, em vez de se basearem em alguma preferência caprichosa ou pessoal.
Telastyn

3
@Telastyn - Tudo sobre o seu comentário parece totalmente louco para mim. Quem deliberadamente tornaria difícil alterar o código? Por que desencorajar os desenvolvedores a alterar o código como acharem melhor - você não confia neles? Eles são maus desenvolvedores?
Benjamin Hodgson

2
De qualquer forma, se a alteração do código tende a ter "efeitos de ondulação", seu código tem um problema de design - nesse caso, os desenvolvedores devem ser incentivados a refatorar o quanto for razoável. Os testes frágeis desencorajam ativamente a refatoração (um teste falha; quem pode se incomodar em descobrir se esse teste foi um dos 80% dos testes que realmente não fazem nada? Você apenas encontra uma maneira diferente e mais complicada de fazer isso). Mas você parece ver isso como uma característica desejável ... eu não entendo nada.
Benjamin Hodgson

2
De qualquer forma, o OP pode achar interessante este post do blog do criador do Rails. Para simplificar bastante o argumento, você provavelmente deve tentar jogar fora esses 80% dos testes.
Benjamin Hodgson

Respostas:


14

Eu acho que é natural encontrar uma divisão dentro dos testes de unidade. Existem muitas opiniões diferentes sobre como fazê-lo corretamente e, naturalmente, todas as outras opiniões estão inerentemente erradas . Recentemente, existem alguns artigos sobre o DrDobbs que exploram esse mesmo problema ao qual eu vinculo no final da minha resposta.

O primeiro problema que vejo nos testes é que é fácil cometer erros. Na aula de C ++ da minha faculdade, fomos expostos a testes de unidade no primeiro e no segundo semestre. Não sabíamos nada sobre programação em geral nos dois semestres - estávamos tentando aprender os fundamentos da programação via C ++. Agora imagine dizer aos alunos: "Ah, ei, você escreveu uma pequena calculadora anual de impostos! Agora escreva alguns testes de unidade para garantir que funcione corretamente". Os resultados devem ser óbvios - todos foram horríveis, incluindo minhas tentativas.

Depois de admitir que você é péssimo em escrever testes de unidade e deseja melhorar, em breve você se deparará com estilos modernos de teste ou diferentes metodologias. Ao testar metodologias, refiro-me a práticas como test-first ou o que Andrew Binstock da DrDobbs faz, que é escrever os testes ao lado do código. Ambos têm seus prós e contras e eu me recuso a entrar em detalhes subjetivos, porque isso incitará uma guerra de chamas. Se você não está confuso sobre qual metodologia de programação é melhor, talvez o estilo de teste faça o truque. Você deve usar TDD, BDD, testes baseados em propriedades? A JUnit possui conceitos avançados chamados Teorias que desfocam a linha entre TDD e testes baseados em propriedades. Qual usar quando?

tl; dr É fácil errar os testes, é incrivelmente opinativo e não acredito que qualquer metodologia de teste seja inerentemente melhor, desde que sejam usadas diligentemente e profissionalmente no contexto em que são apropriadas. Além disso, os testes são na minha opinião, uma extensão a asserções ou testes de sanidade que costumavam garantir uma abordagem ad-hoc rápida e rápida ao desenvolvimento, que agora é muito, muito mais fácil.

Para uma opinião subjetiva, prefiro escrever "fases" de testes, por falta de uma frase melhor. Escrevo testes de unidade que testam classes isoladamente, usando zombarias, quando necessário. Eles provavelmente serão executados com JUnit ou algo semelhante. Então escrevo testes de integração ou aceitação, que são executados separadamente e geralmente apenas algumas vezes por dia. Esse é o seu caso de uso não trivial. Eu normalmente uso o BDD, pois é bom expressar recursos em linguagem natural, algo que o JUnit não pode fornecer facilmente.

Por fim, recursos. Eles apresentarão opiniões conflitantes, principalmente centradas em testes de unidade em diferentes idiomas e com diferentes estruturas. Eles devem apresentar a divisão em ideologia e metodologia, permitindo que você faça sua própria opinião, desde que eu não tenha manipulado muito a sua :)

[1] A corrupção do ágil por Andrew Binstock

[2] Resposta às respostas do artigo anterior

[3] Resposta à corrupção do Agile pelo tio Bob

[4] Resposta à corrupção do Agile por Rob Myers

[5] Por que se preocupar com o teste de pepino?

[6] Você está enganando

[7] Afaste-se das ferramentas

[8] Comentário sobre 'Kata de algarismos romanos com comentário'

[9] Kata de algarismos romanos com comentários


1
Uma das minhas afirmações amigáveis ​​seria que, se você está escrevendo um teste para testar a função de uma calculadora anual de impostos, não está escrevendo um teste de unidade. Esse é um teste de integração. Sua calculadora deve ser dividida em unidades de execução bastante simples, e seus testes unitários testam essas unidades. Se uma dessas unidades parar de funcionar corretamente (o teste começa a falhar), é como derrubar parte de uma parede da fundação e você precisa reparar o código (geralmente não o teste). Ou você identificou um pouco de código que não é mais necessário e deve ser descartado.
Craig

1
@ Craig: Precisamente! Isso é o que eu quis dizer com não saber como escrever testes adequados. Como estudante universitário, o cobrador de impostos era uma classe grande escrita sem o entendimento adequado do SOLID. Você está absolutamente correto ao pensar que este é mais um teste de integração do que qualquer outra coisa, mas esse era um termo desconhecido para nós. Fomos expostos apenas a testes de "unidade" pelo nosso professor.
IAE

5

Eu acredito que é importante ter testes de ambos os tipos e usá-los quando apropriado.

Como você disse, existem dois extremos e eu sinceramente também não concordo com nenhum deles.

A chave é que os testes de unidade precisam cobrir regras e requisitos de negócios . Se houver um requisito de que o sistema deve rastrear a idade de uma pessoa, escreva testes "triviais" para garantir que a idade seja um número inteiro não negativo. Você está testando o domínio de dados exigido pelo sistema: embora trivial, ele tem valor porque está aplicando os parâmetros do sistema .

Da mesma forma, com testes mais complexos, eles precisam agregar valor. Certamente, você pode escrever um teste que valide algo que não é um requisito, mas que deve ser aplicado em uma torre de marfim em algum lugar, mas esse é o tempo gasto melhor escrevendo testes que validam os requisitos pelos quais o cliente está pagando. Por exemplo, por que escrever um teste que valida seu código pode lidar com um fluxo de entrada que atinge o tempo limite, quando os únicos fluxos são de arquivos locais, não da rede?

Acredito firmemente em testes de unidade e uso o TDD onde quer que faça sentido. Os testes de unidade certamente agregam valor na forma de maior qualidade e comportamento "falham rápido" ao alterar o código. No entanto, há também a velha regra 80/20 . Em algum momento, você obterá retornos decrescentes ao escrever testes e precisará avançar para um trabalho mais produtivo, mesmo que exista algum valor mensurável ao escrever mais testes.


Escrever um teste para garantir que um sistema rastreie a idade de uma pessoa não é um teste de unidade, IMO. Esse é um teste de integração. Um teste de unidade testaria a unidade genérica de execução (também conhecida como "procedimento") que, digamos, calcula um valor de idade a partir de, digamos, uma data base e um deslocamento em quaisquer unidades (dias, semanas, etc.). O que quero dizer é que um pouco de código não deve ter nenhuma dependência externa estranha no resto do sistema. APENAS calcula uma idade a partir de alguns valores de entrada e, nesse caso, um teste de unidade pode confirmar o comportamento correto, o que provavelmente causará uma exceção se o deslocamento produzir uma idade negativa.
22414 Craig

Eu não estava me referindo a nenhum cálculo. Se um modelo armazena alguns dados, pode validar que os dados pertencem ao domínio correto. Nesse caso, o domínio é o conjunto de números inteiros não negativos. Os cálculos devem ocorrer no controlador (no MVC) e, neste exemplo, um cálculo de idade seria um teste separado.

4

Aqui está minha opinião: todos os testes têm custos:

  • tempo e esforço iniciais:
    • pense sobre o que testar e como testá-lo
    • implementar o teste e verifique se está testando o que deveria
  • Em manutenção
    • certificando-se de que o teste ainda esteja fazendo o que deveria fazer, pois o código evolui naturalmente
  • executando o teste
    • tempo de execução
    • analisando os resultados

Também pretendemos que todos os testes forneçam benefícios (e, na minha experiência, quase todos os testes fornecem benefícios):

  • especificação
  • destacar casos de canto
  • impedir regressão
  • verificação automática
  • exemplos de uso da API
  • quantificação de propriedades específicas (tempo, espaço)

Portanto, é muito fácil ver que, se você escrever vários testes, eles provavelmente terão algum valor. O ponto em que isso fica complicado é quando você começa a comparar esse valor (que, a propósito, talvez você não saiba com antecedência - se você jogar fora seu código, os testes de regressão perdem o valor) com o custo.

Agora, seu tempo e esforço são limitados. Você gostaria de fazer as coisas que oferecem o maior benefício pelo menor custo. E acho que é uma coisa muito difícil de se fazer, principalmente porque pode exigir conhecimento que não se tem ou seria caro obter.

E esse é o verdadeiro atrito entre essas diferentes abordagens. Eu acredito que todos eles identificaram estratégias de teste que são benéficas. No entanto, cada estratégia tem custos e benefícios diferentes em geral. Além disso, os custos e benefícios de cada estratégia provavelmente dependerão fortemente das especificidades do projeto, do domínio e da equipe. Em outras palavras, pode haver várias melhores respostas.

Em alguns casos, aplicar código sem testes pode fornecer os melhores benefícios / custos. Em outros casos, um conjunto completo de testes pode ser melhor. Ainda em outros casos, melhorar o design pode ser a melhor coisa a fazer.


2

O que é um teste de unidade , realmente? E existe realmente uma grande dicotomia em jogo aqui?

Trabalhamos em um campo em que ler literalmente um pouco além do final de um buffer pode travar totalmente um programa, ou causar um resultado totalmente impreciso, ou conforme evidenciado pelo recente bug TLS "HeartBleed", coloca um sistema supostamente seguro aberto sem produzir nenhuma evidência direta da falha.

É impossível eliminar toda a complexidade desses sistemas. Mas nosso trabalho é, na medida do possível, minimizar e gerenciar essa complexidade.

Um teste de unidade é um teste que confirma, por exemplo, que uma reserva foi lançada com sucesso em três sistemas diferentes, uma entrada de log é criada e uma confirmação por email é enviada?

Eu vou dizer não . Esse é um teste de integração . E esses definitivamente têm seu lugar, mas também são um tópico diferente.

Um teste de integração funciona para confirmar a função geral de um "recurso" inteiro. Mas o código por trás desse recurso deve ser dividido em blocos de construção simples e testáveis, também conhecidos como "unidades".

Portanto, um teste de unidade deve ter um escopo muito limitado.

O que implica que o código testado pelo teste de unidade deve ter um escopo muito limitado.

O que implica ainda que um dos pilares do bom design é decompor seu problema complexo em partes menores e de propósito único (na medida do possível), que podem ser testadas em relativo isolamento um do outro.

O que você acaba criando é um sistema feito de componentes de base confiáveis ​​e você sabe se alguma dessas unidades fundamentais de código quebra porque você escreveu testes simples, pequenos e de escopo limitado para dizer exatamente isso.

Em muitos casos, você provavelmente também deve ter vários testes por unidade. Os testes em si devem ser simples, testando um e apenas um comportamento na medida do possível.

A noção de um "teste de unidade" testando uma lógica complexa não trivial, elaborada é, penso eu, um pouco de oxímoro.

Portanto, se esse tipo de quebra de projeto deliberada ocorreu, como no mundo um teste de unidade poderia repentinamente começar a produzir falsos positivos, a menos que a função básica da unidade de código testada tenha mudado? E se isso aconteceu, é melhor você acreditar que existem alguns efeitos cascata não óbvios em jogo. Seu teste interrompido, que parece estar produzindo um falso positivo, na verdade está avisando que algumas alterações quebraram um círculo mais amplo de dependências na base de código e precisam ser examinadas e corrigidas.

Algumas dessas unidades (muitas delas) podem precisar ser testadas usando objetos simulados, mas isso não significa que você precise escrever testes mais complexos ou elaborados.

Voltando ao meu exemplo artificial de um sistema de reserva, você realmente não pode ser o envio de solicitações de folga para um banco de dados de reserva ao vivo ou serviço de terceiros (ou mesmo um exemplo "dev" dele) cada vez que você unidade de teste de seu código.

Então você usa zombarias que apresentam o mesmo contrato de interface. Os testes podem validar o comportamento de um pedaço de código determinístico relativamente pequeno. Verde todo o tabuleiro diz que os blocos que compõem sua fundação não estão quebrados.

Mas a lógica dos testes unitários individuais permanece a mais simples possível.


1

Obviamente, isso é apenas minha opinião, mas ter passado os últimos meses aprendendo programação funcional no fsharp (vindo de um background em C #) me fez perceber algumas coisas.

Como o OP afirmou, normalmente existem 2 tipos de "testes de unidade" que vemos no dia a dia. Testes que cobrem as entradas e saídas de um método, que geralmente são os mais valiosos, mas são difíceis de executar para 80% do sistema, que se refere menos a "algoritmos" e mais a "abstrações".

O outro tipo, é testar a interatividade da abstração, geralmente envolve zombaria. Na minha opinião, esses testes são principalmente necessários devido ao design do seu aplicativo. Omitindo-os, e você corre o risco de erros estranhos e código spagetti, porque as pessoas não pensam sobre seu design adequadamente, a menos que sejam forçadas a fazer os testes primeiro (e mesmo assim, geralmente estragam tudo). O problema não é tanto a metodologia de teste, mas o design subjacente do sistema. A maioria dos sistemas criados com linguagens imperativas ou OO tem uma dependência inerente aos "efeitos colaterais", também conhecidos como "Faça isso, mas não me diga nada". Quando você confia no efeito colateral, precisa testá-lo, porque um requisito ou operação comercial geralmente faz parte dele.

Quando você projeta seu sistema de uma maneira mais funcional, onde evita criar dependências de efeitos colaterais e evita alterações / rastreamento de estado através da imutabilidade, ele permite que você se concentre mais intensamente nos testes de entrada e saída, que testam claramente mais a ação e menos como você chega lá. Você ficará surpreso com o que coisas como imutabilidade podem oferecer em termos de soluções muito mais simples para os mesmos problemas e, quando você não estiver mais dependente de "efeitos colaterais", poderá executar ações como programação paralela e assíncrona sem quase nenhum custo adicional.

Desde que comecei a codificar no Fsharp, não precisava de uma estrutura de zombaria para nada e até perdi minha dependência completamente de um contêiner do IOC. Meus testes são conduzidos por necessidades e valores de negócios, e não em camadas de abstração pesada, normalmente necessárias para obter composição em programação imperativa.

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.