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, muitas vezes eu coloco valores arbitrários no meu código para ver o que ele faz. Por exemplo, se eu sei que foo(1, 2, 3) deve retornar 17, posso escrever isso:

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

Esses números são puramente arbitrários e não têm significado mais amplo (eles não são, por exemplo, condições de contorno, embora eu teste também sobre eles). Eu lutaria para chegar a bons nomes para esses números, e escrever algo como const int TWO = 2; é obviamente inútil. É aceitável escrever os testes desta forma 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 aceitáveis se o significado for óbvio no contexto, mas, neste caso, os números realmente não têm significado algum.

    
por Kevin 20.06.2016 / 01:01
fonte

11 respostas

81

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

Normalmente, quando os números têm algum significado, você deve atribuí-los às variáveis locais do método de teste para tornar o código mais legível e auto-explicativo. Os nomes das variáveis devem, pelo menos, refletir 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 é denominada HUNDRED_DOLLARS_ZERO_CENT , mas startBalance para indicar qual é o significado da variável, mas não que seu valor seja de alguma forma especial.

    
por 20.06.2016 / 01:04
fonte
20

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

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

Think of a normal unit test as being something like the following:

  1. Set up some data.
  2. Perform some operations on the data.
  3. Assert something about the result.

Hypothesis lets you write tests which instead look like this:

  1. For all data matching some specification.
  2. Perform some operations on the data.
  3. Assert something about the result.

A idéia é não restringir-se a seus próprios valores, mas escolher aleatórios que podem ser usados para verificar se suas funções correspondem às suas 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 fazer por uma entrada arbitrária. 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 não-vazio ou que uma árvore de pesquisa binária de auto-balanceamento é realmente balanceada (usando qualquer critério que a estrutura de dados específica tenha).

No geral, escolher números arbitrários é provavelmente muito ruim - isso não agrega muito valor, e confunde com qualquer outra pessoa que o lê. Gerar automaticamente um monte de dados de teste aleatórios e usá-los efetivamente é bom. Encontrar uma hipótese ou biblioteca QuickCheck-like para o seu idioma de escolha é provavelmente a melhor maneira de alcançar seus objetivos, permanecendo compreensível para os outros.

    
por 20.06.2016 / 04:56
fonte
11

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

Se isso não for suficiente, um pouco de documentação deve ser capaz de fornecê-lo (seja através do nome da variável ou de uma docstring). Tenha em mente que a função em si possui parâmetros que, esperamos, tenham nomes significativos. Copiá-los em seu teste para nomear os argumentos é bastante inútil.

Por último, se os seus testes de unidade forem complicados o suficiente para que isso seja difícil / não prático, você provavelmente terá funções muito complicadas e poderá considerar por que isso acontece.

Quanto mais superficial você escrever testes, pior será o seu código atual. Se você sentir a necessidade de nomear seus valores de teste para tornar o teste claro, ele sugere strongmente que seu método real precisa de uma melhor nomenclatura e / ou documentação. Se você encontrar a necessidade de nomear constantes em testes, eu verificaria porque você precisa disso - provavelmente o problema não é o teste em si, mas a implementação

    
por 20.06.2016 / 04:39
fonte
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 conta própria, mas o caso de teste como um todo é construído de maneira pensada e, portanto, tem um significado específico. É isso que alguém deve documentar de alguma forma. Por exemplo, se foo for realmente um método testForTriangle que decide se os três números podem ser comprimentos válidos das arestas de um triângulo, seus testes podem ter esta aparência:

// 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 de assertEqual , que será exibido quando o teste falhar. Você pode então melhorar isso e refatorar isso em um teste orientado por dados (se sua estrutura de teste suportar isso). No entanto, você faz um favor a si mesmo se colocar uma nota no código porque escolheu esses números e quais dos vários comportamentos que você está testando com o caso individual.

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

por 20.06.2016 / 22:32
fonte
6

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

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

Se você escrever vários testes unitários, cada um com uma variedade de 3 Números (startBalance, interest, years) - eu apenas colocaria os valores no teste unitário como variáveis locais. O menor escopo onde 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 uma linguagem que permite parâmetros nomeados, isso é obviamente superflofo. Lá eu iria apenas empacotar os valores brutos na chamada do método. Não consigo imaginar nenhuma refatoração que torne essa declaração mais concisa:

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

Ou use um framework de teste, que permitirá definir os casos de teste em algum array ou no formato do Mapa:

testcases = { {
                Name: "BigInterest"
               ,StartBalance:       10
               ,InterestInPercent: 100
               ,Years:               2
              }
             ,{ 
                Name: "SmallInterest"
               ,StartBalance:       50
               ,InterestInPercent:  .5
               ,Years:               1
              }
            }
    
por 20.06.2016 / 14:35
fonte
3

...but in this case the numbers actually have no meaning at all

Os números estão sendo usados para chamar um método tão certamente a premissa acima está incorreta. Você não pode cuidar do que são os números, mas isso está além do ponto. Sim, você poderia inferir para que os números são usados por alguma magia do IDE, mas seria muito melhor se você desse apenas os nomes de valores - mesmo que eles apenas correspondam aos parâmetros.

    
por 20.06.2016 / 10:05
fonte
3

Se você quiser testar uma função pura em um conjunto de entradas que não são condições de contorno, então você quase certamente deseja testá-lo em um conjunto inteiro de 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 com, 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 do Dannnno podem ajudá-lo a construir a tabela de valores a serem testados. bar , baz e blurf devem ser substituídos 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 fizer sentido colocar seus números em uma matriz, talvez Em vez disso, se você suspeitar que pode ter dados em mãos, considere colocá-los em uma matriz e adquirir mais deles.

    
por 20.06.2016 / 21:53
fonte
1

Primeiramente vamos concordar que o "teste unitário" é usado frequentemente para cobrir todos os testes automatizados que um programador escreve, e que é inútil debater como cada teste deve ser chamado ...

Trabalhei em um sistema em que o software recebeu muitas entradas e elaborou uma “solução” que precisava cumprir algumas restrições, enquanto otimizava outros números. Não havia respostas certas, então o software só precisou dar uma resposta razoável.

Ele fez isso usando muitos números aleatórios para obter um ponto de partida e depois usando um "alpinista" para melhorar o resultado. Isso foi executado muitas vezes, escolhendo o melhor resultado. Um gerador de números aleatórios pode ser semeado, para que 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 se os resultados foram os mesmos, isso nos disse que não mudamos o que essa parte do sistema fez por engano enquanto refatora etc. Não nos disse nada sobre a exatidão do que aquela parte do sistema fazia.

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

Como nós "ridicularizamos" o banco de dados, você poderia chamar esses testes de "testes unitários", mas a "unidade" era bastante grande.

Geralmente, quando você está trabalhando em um sistema sem testes, faz algo parecido com o acima, para que possa confirmar sua refatoração e não alterar a saída; esperançosamente testes melhores são escritos para novo código!

    
por 20.06.2016 / 13:19
fonte
1

Eu acho 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".

Claro, alguns números mágicos podem ser arbitrários também, como para valores únicos de "manuseio" (que devem ser substituídos por constantes nomeadas, é claro), mas também podem ser constantes pré-calculadas como "velocidade de um pardal europeu vazio em piquetes por quinzena ", onde o valor numérico é conectado sem comentários ou contexto útil.

    
por 22.06.2016 / 17:11
fonte
0

Os testes são diferentes do código de produção e, pelo menos em unidades testadas escritas no Spock, que são curtas e diretas, não tenho problema em usar constantes mágicas.

Se um teste tem 5 linhas de comprimento e segue 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 eu adiciono um usuário chamado Smith, vejo o usuário Smith retornando na lista de usuários", não faz sentido extrair "Smith" para uma constante.

Isto se aplica se você puder facilmente comparar os valores usados no bloco "dado" (setup) com aqueles encontrados nos blocos "when" e "then". Se sua configuração de teste estiver separada (no código) do local em que os dados são usados, talvez seja melhor usar constantes. Mas como os testes são mais auto-contidos, a configuração geralmente está próxima do local de uso e o primeiro caso se aplica, ou seja, as constantes mágicas são bastante aceitáveis nesse caso.

    
por 20.06.2016 / 13:24
fonte
0

Não vou me aventurar a dizer um sim / não definitivo, mas aqui estão algumas perguntas que você deve se fazer ao decidir se está tudo 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 método verify() de Mockito que verifica se determinadas chamadas de método foram feitas para simular objetos em vez de realmente declarar um valor.

  2. Se os números do significam algo, então eles devem ser atribuídos a variáveis nomeadas apropriadamente.

  3. A gravação do número 2 como TWO pode 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, no entanto, seu teste for assertEquals(numCustomersInBank(BANK_1), TWO) , isso não fará muito sentido para esse . Por que o BANK_1 contém dois clientes? O que estamos testando?
por 21.06.2016 / 14:02
fonte