Os números mágicos são aceitáveis ​​em testes de unidade se os números não significam nada?


58

Nos meus testes de unidade, geralmente jogo valores arbitrários no meu código para ver o que ele faz. Por exemplo, se eu souber que foo(1, 2, 3)deve retornar 17, eu poderia escrever o seguinte:

assertEqual(foo(1, 2, 3), 17)

Esses números são puramente arbitrários e não têm um significado mais amplo (eles não são, por exemplo, condições de contorno, embora eu também teste neles). Eu lutaria para criar bons nomes para esses números, e escrever algo como const int TWO = 2;é obviamente inútil. Tudo bem escrever os testes assim ou devo fatorar os números em constantes?

Em Todos os números mágicos são criados da mesma forma? , aprendemos que os números mágicos são bons se o significado for óbvio no contexto, mas, nesse caso, os números realmente não têm significado algum.


9
Se você está inserindo valores e espera poder ler esses mesmos valores, eu diria que os números mágicos são bons. Portanto, se, digamos, 1, 2, 3forem índices de matriz 3D em que você armazenou anteriormente o valor 17, acho que esse teste seria dândi (desde que você também tenha alguns testes negativos). Mas se é o resultado de um cálculo, você deve se certificar de que quem lê este teste vai entender por que foo(1, 2, 3)deve ser 17, e números mágicos provavelmente não vai alcançar essa meta.
Joe White

24
const int TWO = 2;é ainda pior do que apenas usar 2. Está em conformidade com a redação da regra com a intenção de violar seu espírito.
Agent_L 20/06

4
O que é um número que "não significa nada"? Por que estaria no seu código se não significasse nada?
Tim Grant

6
Certo. Deixe um comentário antes de uma série desses testes, por exemplo, "uma pequena seleção de exemplos determinados manualmente". Isso, em relação aos outros testes que estão claramente testando limites e exceções, ficará claro.
Davidbak

5
Seu exemplo é enganoso - quando o nome da sua função seria realmente foo, não significaria nada e, portanto, os parâmetros. Mas, na realidade, eu tenho certeza que a função não tem esse nome, e os parâmetros não têm nomes bar1, bar2e bar3. Faça um exemplo mais realista em que os nomes tenham significado e faça muito mais sentido discutir se os valores dos dados de teste também precisam de um nome.
Doc Brown

Respostas:


80

Quando você realmente tem números que não têm nenhum significado?

Geralmente, quando os números têm algum significado, você deve atribuí-los a variáveis ​​locais do método de teste para tornar o código mais legível e autoexplicativo. Os nomes das variáveis ​​devem refletir pelo menos o que a variável significa, não necessariamente seu valor.

Exemplo:

const int startBalance = 10000;
const float interestRate = 0.05f;
const int years = 5;

const int expectedEndBalance = 12840;

assertEqual(calculateCompoundInterest(startBalance, interestRate, years),
            expectedEndBalance);

Observe que a primeira variável não é nomeada HUNDRED_DOLLARS_ZERO_CENT, mas startBalancepara indicar qual é o significado da variável, mas não que seu valor seja de alguma forma especial.


3
@ Kevin - em que idioma você está testando? Algumas estruturas de teste permitem que você configure provedores de dados que retornam uma matriz de matrizes de valores para o teste
HorusKol

10
Embora eu concorde com a idéia, cuidado com o fato de que essa prática também pode introduzir novos erros, como se você acidentalmente extraísse um valor como 0.05fum int. :)
Jeff Bowman

5
+1 - ótimas coisas. Só porque você não se importa que um valor particular é, isso não significa que não é ainda um número mágico ...
Robbie Dee

2
@ PieterB: AFAIK é culpa de C e C ++, que formalizou a noção de uma constvariável.
Steve Jessop

2
Você nomeou suas variáveis ​​da mesma forma que os parâmetros nomeados de calculateCompoundInterest? Nesse caso, a digitação extra é uma prova de trabalho que você leu a documentação para a função que está testando ou, pelo menos, copiou os nomes dados pelo seu IDE. Não sei ao certo o que isso diz ao leitor sobre a intenção do código, mas se você passar os parâmetros na ordem errada, pelo menos eles poderão dizer o que foi planejado.
Steve Jessop

20

Se você estiver usando números arbitrários apenas para ver o que eles fazem, provavelmente o que você está procurando é provavelmente dados de teste gerados aleatoriamente ou testes baseados em propriedades.

Por exemplo, a Hipótese é uma biblioteca Python legal para esse tipo de teste e é baseada no QuickCheck .

Pense em um teste de unidade normal como algo como o seguinte:

  1. Configure alguns dados.
  2. Execute algumas operações nos dados.
  3. Afirme algo sobre o resultado.

A hipótese permite que você escreva testes que se parecem com isso:

  1. Para todos os dados correspondentes a alguma especificação.
  2. Execute algumas operações nos dados.
  3. Afirme algo sobre o resultado.

A idéia é não se restringir a seus próprios valores, mas escolha valores aleatórios que possam ser usados ​​para verificar se suas funções correspondem às especificações. Como uma observação importante, esses sistemas geralmente lembram de qualquer entrada que falhe e, em seguida, garantem que essas entradas sejam sempre testadas no futuro.

O ponto 3 pode ser confuso para algumas pessoas, então vamos esclarecer. Isso não significa que você está afirmando a resposta exata - isso é obviamente impossível de ser feito para contribuições arbitrárias. Em vez disso, você afirma algo sobre uma propriedade do resultado. Por exemplo, você pode afirmar que, depois de anexar algo a uma lista, ele se torna vazio, ou que uma árvore de pesquisa binária com balanceamento automático é realmente equilibrada (usando qualquer critério que a estrutura de dados específica tenha).

No geral, escolher números arbitrários é provavelmente muito ruim - isso realmente não agrega muito valor e é confuso para quem lê. Gerar automaticamente um monte de dados de teste aleatórios e usá-los efetivamente é bom. Encontrar uma biblioteca do tipo Hipótese ou QuickCheck para o seu idioma preferido é provavelmente a melhor maneira de atingir seus objetivos, mantendo-se compreensível para os outros.


11
O teste aleatório pode encontrar erros difíceis de reproduzir, mas o teste aleatório dificilmente encontra erros reproduzíveis. Certifique-se de capturar todas as falhas de teste com um caso de teste reproduzível específico.
JBRWilkinson

5
E como você sabe que o seu teste de unidade não é corrigido quando você "afirma algo sobre o resultado" (neste caso, recalcula o que fooé computação) ...? Se você tivesse 100% de certeza de que seu código fornece a resposta certa, basta inserir esse código no programa e não testá-lo. Se você não estiver, precisará testar o teste, e acho que todo mundo vê para onde isso está indo.

2
Sim, se você passa entradas aleatórias para uma função, precisa saber qual seria a saída para poder afirmar que está funcionando corretamente. Com valores de teste fixos / escolhidos, é claro que você pode trabalhar manualmente, etc., mas certamente qualquer método automatizado de detecção, se o resultado estiver correto, está sujeito aos mesmos problemas que a função que você está testando. Você usa a implementação que possui (o que não pode porque está testando se funciona) ou escreve uma nova implementação com a mesma probabilidade de ser de buggy (ou mais, caso contrário, você usaria a mais provável) )
Chris

7
@NajibIdrissi - não necessariamente. Você pode, por exemplo, testar se a aplicação do inverso da operação que você está testando no resultado devolve o valor inicial com o qual você começou. Invariantes ou você poderia testar esperados (por exemplo, para todos os cálculos de juros em ddias, o cálculo de ddias + 1 mês deve ser um conhecido taxa mensal percentagem mais elevada), etc.
Jules

12
@ Chris - Em muitos casos, verificar os resultados está correto é mais fácil do que gerar os resultados. Embora isso não seja verdade em todas as circunstâncias, há muitas onde está. Exemplo: adicionar uma entrada a uma árvore binária equilibrada deve resultar em uma nova árvore que também seja equilibrada ... fácil de testar, bastante complicada de implementar na prática.
Jules

11

O nome do seu teste de unidade deve fornecer a maior parte do contexto. Não a partir dos valores das constantes. O nome / documentação de um teste deve fornecer o contexto e a explicação apropriados para quaisquer números mágicos presentes no teste.

Se isso não for suficiente, um pouco de documentação deve ser capaz de fornecê-la (seja por meio do nome da variável ou de uma doutrina). Lembre-se de que a própria função possui parâmetros que esperamos ter nomes significativos. Copiar aqueles em seu teste para nomear os argumentos é inútil.

E por último, se suas unittests são complicadas o suficiente para que isso seja difícil / não prático, você provavelmente tem funções muito complicadas e pode considerar por que esse é o caso.

Quanto mais desleixado você escrever testes, pior será o seu código real. Se você sentir a necessidade de nomear seus valores de teste para torná-lo claro, isso sugere fortemente que seu método real precisa de nomes e / ou documentação melhores. Se você encontrar a necessidade de nomear constantes nos testes, examinarei por que você precisa disso - provavelmente o problema não é o teste em si, mas a implementação


Esta resposta parece ser sobre a dificuldade de inferir o propósito de um teste ao passo que a questão real é sobre números mágicos dos parâmetros do método ...
Robbie Dee

@RobbieDee, o nome / documentação de um teste deve fornecer o contexto e a explicação adequados de quaisquer números mágicos presentes no teste. Caso contrário, adicione a documentação ou renomeie o teste para ficar mais claro.
Enderland

Ainda seria melhor dar nomes aos números mágicos. Se o número de parâmetros for alterado, a documentação corre o risco de ficar desatualizada.
Robbie Dee

11
@RobbieDee lembre-se de que a função em si possui parâmetros que esperamos ter nomes significativos. Copiar aqueles em seu teste para nomear os argumentos é inútil.
enderland

"Espero que sim" hein? Por que não apenas codificar a coisa corretamente e acabar com o que é ostensivamente um número mágico como Philipp já delineado ...
Robbie Dee

9

Isso depende muito da função que você está testando. Conheço muitos casos em que os números individuais não têm um significado especial por si só, mas o caso de teste como um todo é construído cuidadosamente e, portanto, tem um significado específico. É isso que se deve documentar de alguma maneira. Por exemplo, se foorealmente for um método testForTriangleque decida se os três números podem ter comprimentos válidos das arestas de um triângulo, seus testes podem ser assim:

// standard triangle with area >0
assertEqual(testForTriangle(2, 3, 4), true);

// degenerated triangle, length of two edges match the length of the third
assertEqual(testForTriangle(1, 2, 3), true);  

// no triangle
assertEqual(testForTriangle(1, 2, 4), false); 

// two sides equal
assertEqual(testForTriangle(2, 2, 3), true);

// all three sides equal
assertEqual(testForTriangle(4, 4, 4), true);

// degenerated triangle / point
assertEqual(testForTriangle(0, 0, 0), true);  

e assim por diante. Você pode melhorar isso e transformar os comentários em um parâmetro de mensagem assertEqualque será exibido quando o teste falhar. Você pode melhorar ainda mais e refatorá-lo em um teste orientado a dados (se sua estrutura de teste suportar isso). No entanto, você faz um favor a si mesmo se colocar uma nota no código por que escolheu esses números e qual dos vários comportamentos que está testando com o caso individual.

É claro que, para outras funções, os valores individuais dos parâmetros podem ser mais importantes, portanto, usar um nome de função sem sentido, como fooao perguntar como lidar com o significado dos parâmetros, provavelmente não é a melhor idéia.


Solução sensata.
user1725145

6

Por que queremos usar constantes nomeadas em vez de números?

  1. SECO - Se eu precisar do valor em 3 locais, quero defini-lo apenas uma vez, para poder alterá-lo em um local, se ele mudar.
  2. Dê significado aos números.

Se você escrever vários testes de unidade, cada um com uma variedade de 3 números (startBalance, juros, anos) - eu apenas agruparia os valores no teste de unidade como variáveis ​​locais. O menor escopo ao qual eles pertencem.

testBigInterest()
  var startBalance = 10;
  var interestInPercent = 100
  var years = 2
  assert( calcCreditSum( startBalance, interestInPercent, years ) == 40 )

testSmallInterest()
  var startBalance = 50;
  var interestInPercent = .5
  var years = 1
  assert( calcCreditSum( startBalance, interestInPercent, years ) == 50.25 )

Se você usa um idioma que permite parâmetros nomeados, isso é obviamente supérfluo. Lá, eu apenas empacotaria os valores brutos na chamada do método. Não consigo imaginar nenhuma refatoração tornando essa declaração mais concisa:

testBigInterest()
  assert( calcCreditSum( startBalance:       10
                        ,interestInPercent: 100
                        ,years:               2 ) = 40 )

Ou use uma estrutura de teste, que permitirá definir os casos de teste em algum array ou formato de mapa:

testcases = { {
                Name: "BigInterest"
               ,StartBalance:       10
               ,InterestInPercent: 100
               ,Years:               2
              }
             ,{ 
                Name: "SmallInterest"
               ,StartBalance:       50
               ,InterestInPercent:  .5
               ,Years:               1
              }
            }

3

... mas, neste caso, os números realmente não têm significado algum

Os números estão sendo usados ​​para chamar um método, portanto, certamente a premissa acima está incorreta. Você pode não se importar com os números, mas isso não vem ao caso. Sim, você pode deduzir para que os números são usados ​​por algumas magias do IDE, mas seria muito melhor se você fornecesse apenas os nomes dos valores - mesmo que eles correspondam aos parâmetros.


11
Porém, isso não é necessariamente verdade - como no exemplo do teste de unidade mais recente que escrevi ( assertEqual "Returned value" (makeKindInt 42) (runTest "lvalue_operators")). Neste exemplo, 42é apenas um valor de espaço reservado que é produzido pelo código no script de teste nomeado lvalue_operatorse verificado quando é retornado pelo script. Não tem nenhum significado, exceto que o mesmo valor ocorre em dois lugares diferentes. Qual seria um nome apropriado aqui que real dê algum significado útil?
Jules

3

Se você deseja testar uma função pura em um conjunto de entradas que não são condições de contorno, quase certamente deseja testá-lo em vários conjuntos de entradas que não são (e são) condições de contorno. E para mim isso significa que deve haver uma tabela de valores para chamar a função e um loop:

struct test_foo_values {
    int bar;
    int baz;
    int blurf;
    int expected;
};
const struct test_foo_values test_foo_with[] = {
   { 1, 2, 3, 17 },
   { 2, 4, 9, 34 },
   // ... many more here ...
};

for (size_t i = 0; i < ARRAY_SIZE(test_foo_with); i++) {
    const struct test_foo_values *c = test_foo_with[i];
    assertEqual(foo(c->bar, c->baz, c->blurf), c->expected);
}

Ferramentas como as sugeridas na resposta de Dannnno podem ajudá-lo a construir a tabela de valores a serem testados. bar,, baze blurfdeve ser substituído por nomes significativos, conforme discutido na resposta de Philipp .

(Princípio geral discutível aqui: os números nem sempre são "números mágicos" que precisam de nomes; em vez disso, os números podem ser dados . Se faria sentido colocar seus números em uma matriz, talvez uma matriz de registros, provavelmente são dados Por outro lado, se você suspeitar que possa ter dados em suas mãos, considere colocá-los em uma matriz e adquiri-los.)


1

Os testes são diferentes do código de produção e, pelo menos nos testes de unidades escritos em Spock, que são curtos e direto ao ponto, não tenho problemas em usar constantes mágicas.

Se um teste tiver 5 linhas e seguir o esquema básico dado / quando / então, extrair esses valores em constantes só tornaria o código mais longo e difícil de ler. Se a lógica for "Quando adiciono um usuário chamado Smith, vejo o usuário Smith retornado na lista de usuários", não faz sentido extrair "Smith" para uma constante.

É claro que isso se aplica se você puder facilmente corresponder os valores usados ​​no bloco "fornecido" (configuração) aos encontrados nos blocos "quando" e "então". Se sua configuração de teste estiver separada (em código) do local em que os dados são usados, pode ser melhor usar constantes. Porém, como os testes são mais independentes, a instalação geralmente fica próxima ao local de uso e o primeiro caso se aplica, o que significa que constantes mágicas são bastante aceitáveis ​​nesse caso.


1

Em primeiro lugar, vamos concordar que o "teste de unidade" é freqüentemente usado para cobrir todos os testes automatizados que um programador escreve e que não faz sentido debater como cada teste deve ser chamado….

Eu trabalhei em um sistema em que o software utilizou muitas entradas e elaborou uma “solução” que precisava cumprir algumas restrições, além de otimizar outros números. Como não havia respostas corretas, o software precisou dar uma resposta razoável.

Isso foi feito usando muitos números aleatórios para obter um ponto de partida e, em seguida, usando um "alpinista" para melhorar o resultado. Isso foi executado várias vezes, escolhendo o melhor resultado. Um gerador de números aleatórios pode ser semeado, para que ele sempre forneça os mesmos números na mesma ordem; portanto, se o teste definir uma semente, sabemos que o resultado seria o mesmo em cada execução.

Tivemos muitos testes que fizeram o acima, e verificamos que os resultados eram os mesmos. Isso nos dizia que não havíamos mudado o que aquela parte do sistema fazia por engano durante a refatoração, etc. Isso não nos dizia nada sobre a exatidão dos o que essa parte do sistema fez.

Esses testes eram caros de manter, pois qualquer alteração no código de otimização os interromperia, mas eles também encontraram alguns erros no código muito maior que pré-processou os dados e pós-processou os resultados.

Como “zombamos” do banco de dados, você pode chamar esses testes de “testes unitários”, mas a “unidade” era bastante grande.

Frequentemente, quando você está trabalhando em um sistema sem testes, faz algo como o descrito acima, para confirmar que sua refatoração não altera a saída; espero que melhores testes sejam escritos para o novo código!


1

Penso que, neste caso, os números devem ser denominados Números Arbitrários, em vez de Números Mágicos, e apenas comentar a linha como "caso de teste arbitrário".

Certamente, alguns Números Mágicos também podem ser arbitrários, como para valores únicos de "manipulação" (que devem ser substituídos por constantes nomeadas, é claro), mas também podem ser constantes pré-calculadas como "velocidade no ar de um pardal europeu sem carga em quinze dias por quinzena", onde o valor numérico é conectado sem comentários ou contexto útil.


0

Não vou me arriscar a dizer um sim / não definitivo, mas aqui estão algumas perguntas que você deve se perguntar ao decidir se está bem ou não.

  1. Se os números não significam nada, por que eles estão lá em primeiro lugar? Eles podem ser substituídos por outra coisa? Você pode fazer a verificação com base em chamadas de método e fluxo, em vez de declarações de valor? Considere algo como o verify()método de Mockito que verifica se determinadas chamadas de método foram ou não feitas para zombar de objetos em vez de realmente afirmar um valor.

  2. Se os números de fazer significar alguma coisa, então eles devem ser atribuídos a variáveis que são nomeados de forma adequada.

  3. Escrevendo o número 2como TWOpode ser útil em determinados contextos, e não tanto em outros contextos.

    • Por exemplo: assertEquals(TWO, half_of(FOUR))faz sentido para alguém que lê o código. É imediatamente claro o que você está testando.
    • Se, contudo, o teste é assertEquals(numCustomersInBank(BANK_1), TWO), então isso não faz que muito sentido. Por que BANK_1contém dois clientes? Para que estamos testando?
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.