Допустимы ли магические числа в модульных тестах, если числа ничего не значат?

58

В моих модульных тестах я часто выбрасываю произвольные значения в свой код, чтобы посмотреть, что он делает. Например, если я знаю, что foo(1, 2, 3)должен возвращать 17, я мог бы написать это:

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

Эти числа являются чисто произвольными и не имеют более широкого значения (они не являются, например, граничными условиями, хотя я также проверяю их). Я бы изо всех сил пытался придумать хорошие имена для этих чисел, и писать что-то вроде этого const int TWO = 2;, очевидно, бесполезно. Можно ли писать такие тесты, или я должен разделить числа на константы?

В Все ли магические числа созданы одинаково? мы узнали, что магические числа в порядке, если смысл очевиден из контекста, но в этом случае числа фактически не имеют никакого значения.

Kevin
источник
9
Если вы вводите значения и ожидаете, что сможете прочитать те же значения обратно, я бы сказал, магические числа в порядке. Так что, если, скажем, 1, 2, 3это индексы трехмерного массива, в которых вы ранее сохранили значение 17, то я думаю, что этот тест будет отличным (если у вас есть и отрицательные тесты). Но если это результат вычислений, вы должны убедиться, что любой, кто читает этот тест, поймет, почему так foo(1, 2, 3)должно быть 17, и магические числа, вероятно, не достигнут этой цели.
Джо Уайт
24
const int TWO = 2;это даже хуже, чем просто использовать 2. Это соответствует формулировке правила с намерением нарушить его дух.
Agent_L
4
Что такое число, которое «ничего не значит»? Почему это будет в вашем коде, если это ничего не значит?
Тим Грант
6
Конечно. Оставьте комментарий перед серией таких тестов, например, «небольшая подборка определенных вручную примеров». Это будет понятно по отношению к другим вашим тестам, в которых четко определены границы и исключения.
Давидбак
5
Ваш пример вводит в заблуждение - когда имя вашей функции будет действительно foo, это ничего не будет значить, и поэтому параметры. Но на самом деле, я уверен , что функция не имеет это имя, и эти параметры не имеют названия bar1, bar2и bar3. Сделайте более реалистичный пример, в котором имена имеют смысл, и тогда будет гораздо разумнее обсудить, нужно ли имени для тестовых данных тоже имя.
Док Браун

Ответы:

80

Когда у вас действительно есть числа, которые не имеют никакого значения вообще?

Обычно, когда числа имеют какое-либо значение, вы должны назначить их локальным переменным тестового метода, чтобы сделать код более читабельным и понятным. Имена переменных должны как минимум отражать значение переменной, а не обязательно ее значение.

Пример:

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

const int expectedEndBalance = 12840;

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

Обратите внимание, что первая переменная не названа HUNDRED_DOLLARS_ZERO_CENT, но startBalanceдля обозначения значения переменной, но не в том, что ее значение каким-либо образом особенное.

Philipp
источник
3
@Kevin - на каком языке ты тестируешь? Некоторые среды тестирования позволяют вам устанавливать поставщиков данных, которые возвращают массив массивов значений для тестирования
HorusKol,
10
Хотя я согласен с этой идеей, имейте в виду, что эта практика также может приводить к появлению новых ошибок, например, если вы случайно извлекаете значение, например, 0.05fдля int. :)
Джефф Боуман
5
+1 - отличный материал. То, что вам не важно, что представляет собой конкретная ценность, вовсе не означает, что это не магическое число ...
Робби Ди,
2
@PieterB: AFAIK, это вина C и C ++, которые формализовали понятие constпеременной.
Стив Джессоп
2
Вы назвали свои переменные так же, как названные параметры calculateCompoundInterest? Если это так, то дополнительная проверка является доказательством того, что вы прочитали документацию для функции, которую вы тестируете, или, по крайней мере, скопировали имена, предоставленные вам вашей IDE. Я не уверен, насколько это говорит читателю о намерениях кода, но если вы передадите параметры в неправильном порядке, они, по крайней мере, смогут сказать, что было задумано.
Стив Джессоп
20

Если вы используете произвольные числа только для того, чтобы посмотреть, что они делают, то, что вы действительно ищете, это, вероятно, случайно сгенерированные тестовые данные или тестирование на основе свойств.

Например, Hypothesis - это классная библиотека Python для такого рода тестирования, основанная на QuickCheck .

Думайте о нормальном модульном тесте как о чем-то вроде следующего:

  1. Настройте некоторые данные.
  2. Выполните некоторые операции с данными.
  3. Утвердите что-нибудь о результате.

Гипотеза позволяет вам писать тесты, которые вместо этого выглядят так:

  1. Для всех данных, соответствующих некоторой спецификации.
  2. Выполните некоторые операции с данными.
  3. Утвердите что-нибудь о результате.

Идея состоит в том, чтобы не ограничивать себя собственными значениями, а выбирать случайные значения, которые можно использовать для проверки соответствия ваших функций их спецификациям. В качестве важного примечания, эти системы, как правило, запоминают любые входные данные, которые дают сбой, и затем гарантируют, что эти входные данные всегда проверяются в будущем.

Пункт 3 может сбивать с толку некоторых людей, поэтому давайте уточним. Это не означает, что вы утверждаете точный ответ - это очевидно невозможно сделать для произвольного ввода. Вместо этого вы утверждаете что-то о свойстве результата. Например, вы можете утверждать, что после добавления чего-либо в список оно становится непустым или что само сбалансированное двоичное дерево поиска фактически сбалансировано (с использованием любых критериев, которые имеет конкретная структура данных).

В целом, выбор произвольных чисел сам по себе, вероятно, довольно плохой - он на самом деле не добавляет всей ценности и вводит в заблуждение любого, кто его читает. Автоматически генерировать кучу случайных тестовых данных и эффективно их использовать - это хорошо. Поиск гипотезы или библиотеки, подобной QuickCheck, для вашего языка - это, вероятно, лучший способ достичь ваших целей, оставаясь при этом понятным для других.

Dannnno
источник
11
Случайное тестирование может обнаружить ошибки, которые трудно воспроизвести, но случайное тестирование вряд ли найдет воспроизводимые ошибки. Обязательно фиксируйте любые неудачи теста с помощью конкретного воспроизводимого контрольного примера.
Дж.Б. Уилкинсон
5
И откуда вы знаете, что ваш модульный тест не прослушивается, когда вы «утверждаете что-то о результате» (в этом случае пересчитайте, что fooтакое вычисления) ...? Если бы вы были на 100% уверены, что ваш код дает правильный ответ, то вы просто поместили бы этот код в программу, а не тестировали его. Если нет, то вам нужно протестировать тест, и я думаю, что все видят, куда это идет.
2
Да, если вы передаете случайные входные данные в функцию, вы должны знать, какой будет выход, чтобы иметь возможность утверждать, что он работает правильно. С фиксированными / выбранными значениями теста вы можете, конечно, отработать его вручную и т. Д., Но, безусловно, любой автоматический метод определения правильности результата подвержен тем же проблемам, что и функция, которую вы тестируете. Вы либо используете имеющуюся реализацию (которую вы не можете, потому что тестируете, работает ли она), либо вы пишете новую реализацию, которая с такой же вероятностью будет содержать ошибки (или более того, в противном случае вы будете использовать более вероятную, чтобы быть правильной). ).
Крис
7
@NajibIdrissi - не обязательно. Например, вы можете проверить, что применение обратной операции, которую вы проверяете, к результату возвращает начальное значение, с которого вы начали. Или вы можете протестировать ожидаемые инварианты (например, для всех расчетов процентов по dдням, расчет по dдням + 1 месяц должен быть выше известной месячной процентной ставки) и т. Д.
Жюль
12
@Chris - во многих случаях проверка правильности результатов проще, чем генерация результатов. Хотя это не так при любых обстоятельствах, есть много где это. Пример: добавление записи в сбалансированное бинарное дерево должно привести к созданию нового дерева, которое также будет сбалансированным ... его легко протестировать, довольно сложно реализовать на практике.
Жюль
11

Ваше имя модульного теста должно обеспечивать большую часть контекста. Не из значений констант. Название / документация для теста должны содержать соответствующий контекст и объяснение того, какие магические числа присутствуют в тесте.

Если этого недостаточно, небольшая часть документации должна быть в состоянии предоставить его (через имя переменной или строку документации). Имейте в виду, что сама функция имеет параметры, которые, как мы надеемся, имеют значимые имена. Копировать их в свой тест для именования аргументов довольно бессмысленно.

И, наконец, если ваши юнит-тесты достаточно сложны, что это сложно / не практично, вы, вероятно, имеете слишком сложные функции и можете подумать, почему это так.

Чем более небрежно вы пишете тесты, тем хуже будет ваш реальный код. Если вы чувствуете необходимость назвать свои значения теста, чтобы сделать тест понятным, это настоятельно рекомендует вашему фактическому методу требовать лучшего наименования и / или документации. Если вы обнаружите необходимость называть константы в тестах, я выясню, зачем вам это нужно - скорее всего, проблема не в самом тесте, а в его реализации.

enderland
источник
Похоже, что этот ответ связан с трудностью определения цели теста, тогда как реальный вопрос касается магических чисел в параметрах метода ...
Робби Ди,
@RobbieDee название / документация для теста должна содержать соответствующий контекст и объяснение того, какие магические числа присутствуют в тесте. Если нет, то добавьте документацию или переименуйте тест, чтобы сделать его более понятным.
enderland
Было бы все же лучше дать имена магических чисел. Если число параметров будет изменено, документация может устареть.
Робби Ди
1
@RobbieDee имейте в виду, что сама функция имеет параметры, которые, как мы надеемся, имеют значимые имена. Копировать их в свой тест для именования аргументов довольно бессмысленно.
enderland
"Надеюсь", а? Почему бы просто не закодировать это правильно и не покончить с тем, что якобы является магическим числом, как Филипп уже обрисовал ...
Робби Ди
9

Это сильно зависит от функции, которую вы тестируете. Я знаю много случаев, когда отдельные числа сами по себе не имеют особого значения, но контрольный пример в целом построен продуманно и поэтому имеет особое значение. Вот что нужно каким-то образом документировать. Например, если на fooсамом деле это метод, testForTriangleкоторый решает, могут ли три числа быть действительными длинами краев треугольника, ваши тесты могут выглядеть следующим образом:

// 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);  

и так далее. Вы можете улучшить это и превратить комментарии в сообщение, параметр assertEqualкоторого будет отображаться при сбое теста. Затем вы можете улучшить это и преобразовать в тест, управляемый данными (если это поддерживается вашей средой тестирования). Тем не менее, вы делаете себе одолжение, если вносите в код примечание, почему вы выбрали эти цифры и какое из различных поведений вы тестируете в каждом конкретном случае.

Конечно, для других функций отдельные значения параметров могут иметь большее значение, поэтому использование бессмысленного имени функции, например, fooкогда спрашиваете, как работать со значением параметров, вероятно, не лучшая идея.

Док Браун
источник
Разумное решение.
user1725145
6

Почему мы хотим использовать именованные константы вместо чисел?

  1. СУХОЙ - если мне нужно значение в 3 местах, я хочу определить его только один раз, поэтому я могу изменить его в одном месте, если оно изменится.
  2. Придай смысл числам.

Если вы напишете несколько модульных тестов, каждый из которых имеет ассортимент из 3 чисел (startBalance, процент, годы) - я просто упаковал бы значения в модульный тест как локальные переменные. Наименьшая сфера, где они принадлежат.

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 )

Если вы используете язык, который допускает именованные параметры, это, конечно, излишне. Там я просто упаковал бы необработанные значения в вызов метода. Я не могу представить какой-либо рефакторинг, делающий это утверждение более кратким:

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

Или используйте тестирующий фреймворк, который позволит вам определять тестовые случаи в некотором массиве или формате карты:

testcases = { {
                Name: "BigInterest"
               ,StartBalance:       10
               ,InterestInPercent: 100
               ,Years:               2
              }
             ,{ 
                Name: "SmallInterest"
               ,StartBalance:       50
               ,InterestInPercent:  .5
               ,Years:               1
              }
            }
Falco
источник
3

... но в этом случае цифры на самом деле не имеют никакого значения вообще

Числа используются для вызова метода, поэтому, безусловно, приведенная выше предпосылка неверна. Вам может быть все равно, какие цифры, но это не относится к делу. Да, вы могли бы определить, для чего используются числа в некоторых IDE-мастерах, но было бы гораздо лучше, если бы вы просто дали имена значений - даже если они просто соответствуют параметрам.

Робби Ди
источник
1
Однако это не обязательно так - как в примере с последним модульным тестом, который я написал ( assertEqual "Returned value" (makeKindInt 42) (runTest "lvalue_operators")). В этом примере 42это просто значение заполнителя, которое создается кодом в тестовом сценарии с именем lvalue_operatorsи затем проверяется, когда оно возвращается сценарием. Это не имеет никакого значения, за исключением того, что одно и то же значение встречается в двух разных местах. Какое здесь подходящее имя может дать какое-либо полезное значение?
Жюль
3

Если вы хотите протестировать чистую функцию на одном наборе входных данных, которые не являются граничными условиями, то вы почти наверняка захотите протестировать ее на всем наборе входных данных, которые не являются (и являются) граничными условиями. И для меня это означает, что должна быть таблица значений для вызова функции и цикл:

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);
}

Инструменты, подобные тем, которые предлагаются в ответе Даннно, могут помочь вам составить таблицу значений для тестирования. bar, bazИ blurfдолжны быть заменены на значимые имена , как описано в ответ Филиппа в .

(Здесь можно утверждать общий принцип: числа не всегда являются «магическими числами», которым нужны имена; вместо этого числа могут быть данными . Если имеет смысл поместить ваши числа в массив, возможно, в массив записей, то это, вероятно, данные И наоборот, если вы подозреваете, что у вас могут быть данные, попробуйте поместить их в массив и получить больше.)

zwol
источник
1

Тесты отличаются от производственного кода, и, по крайней мере, в модульных тестах, написанных на Spock, которые являются короткими, и у меня нет проблем с использованием магических констант.

Если тест состоит из 5 строк и следует базовой схеме «дано / когда / тогда», извлечение таких значений в константы только сделает код длиннее и сложнее для чтения. Если логика звучит так: «Когда я добавляю пользователя по имени Смит, я вижу, что пользователь Смит вернулся в список пользователей», нет смысла извлекать «Смит» из константы.

Это, конечно, применимо, если вы можете легко сопоставить значения, используемые в блоке «задано» (настройка), со значениями, найденными в блоках «когда» и «затем». Если ваша тестовая установка отделена (в коде) от места, где используются данные, возможно, лучше использовать константы. Но поскольку тесты лучше всего автономны, настройка обычно близка к месту использования, и применяется первый случай, означающий, что магические константы в этом случае вполне приемлемы.

Михал Космульский
источник
1

Во-первых, давайте согласимся, что «модульный тест» часто используется для охвата всех автоматических тестов, которые пишет программист, и что бессмысленно спорить о том, как называть каждый тест….

Я работал над системой, в которой программное обеспечение брало много входных данных и разработало «решение», которое должно было выполнять некоторые ограничения при оптимизации других чисел. Правильных ответов не было, поэтому программное обеспечение должно было дать разумный ответ.

Это было сделано с помощью множества случайных чисел, чтобы получить отправную точку, а затем с помощью «альпиниста холма», чтобы улучшить результат. Это было выполнено много раз, выбирая лучший результат. Генератор случайных чисел может быть заполнен так, что он всегда выдает одинаковые числа в одном и том же порядке, поэтому, если тест устанавливает начальное число, мы знаем, что результат будет одинаковым при каждом запуске.

У нас было много тестов, которые сделали вышеупомянутое, и проверили, что результаты были такими же, это сказало нам, что мы не изменили то, что эта часть системы сделала по ошибке при рефакторинге и т. Д. Это не сказало нам ничего о правильности что сделала эта часть системы.

Эти тесты были дорогостоящими в обслуживании, так как любое изменение в оптимизирующем коде могло нарушить тесты, но они также обнаружили некоторые ошибки в гораздо более крупном коде, который предварительно обрабатывал данные и постобработал результаты.

Поскольку мы «издевались» над базой данных, вы могли бы назвать эти тесты «модульными тестами», но «блок» был довольно большим.

Часто, когда вы работаете в системе без тестов, вы делаете что-то подобное выше, так что вы можете подтвердить, что ваш рефакторинг не меняет вывод; надеюсь, лучшие тесты написаны для нового кода!

Ян
источник
1

Я думаю, что в этом случае числа следует называть произвольными числами, а не магическими числами, и просто прокомментируйте строку как «произвольный контрольный пример».

Конечно, некоторые Магические Числа также могут быть произвольными, например, для уникальных «дескрипторных» значений (которые, конечно, должны быть заменены именованными константами), но также могут быть заранее вычисленными константами, такими как «воздушная скорость порожнего европейского воробья в Фарлонгах за две недели», где числовое значение вставляется без комментариев или полезного контекста.

RufusVS
источник
0

Я не буду рисковать настолько, чтобы сказать однозначно да / нет, но вот некоторые вопросы, которые вы должны задать себе, решая, все ли в порядке или нет.

  1. Если цифры ничего не значат, то почему они там в первую очередь? Могут ли они быть заменены чем-то другим? Можете ли вы сделать проверку на основе вызовов методов и потоков вместо утверждений значений? Рассмотрим что-то похожее на verify()метод Mockito, который проверяет, были ли сделаны определенные вызовы метода для имитации объектов, вместо того, чтобы фактически утверждать значение.

  2. Если числа действительно что-то значат, то они должны быть присвоены переменным, которые имеют соответствующие имена.

  3. Запись числа , 2как TWOмогло бы быть полезным в определенных условиях, и не столько в других контекстах.

    • Например: assertEquals(TWO, half_of(FOUR))имеет смысл для того, кто читает код. Сразу понятно, что вы тестируете.
    • Однако , если ваш тест assertEquals(numCustomersInBank(BANK_1), TWO), то это не делает этого особого смысла. Почему же BANK_1содержит два клиента? Для чего мы тестируем?
Арнаб Датта
источник