Чи прийнятні магічні числа в одиничних тестах, якщо цифри нічого не означають?


59

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

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

Ці числа суто довільні і не мають ширшого значення (наприклад, це не є граничними умовами, хоча я і тестую їх). Я б намагався придумати хороші імена для цих номерів, і писати щось подібне const int TWO = 2;явно не допомагає. Чи правильно писати такі тести, або я повинен розбивати числа на константи?

В Чи всі магічні числа створені однаковими? , ми дізналися, що магічні числа в порядку, якщо значення очевидно з контексту, але в цьому випадку числа насправді взагалі не мають значення.


9
Якщо ви вводите значення і очікуєте, що зможете прочитати ті самі значення назад, я б сказав, що магічні числа добре. Отже, якщо, скажімо, 1, 2, 3є індекси 3D-масивів, де ви раніше зберігали значення 17, то я думаю, що цей тест був би дендім (доки ви також маєте негативні тести). Але якщо це результат підрахунку, ви повинні переконатися, що хтось, хто читає цей тест, зрозуміє, чому foo(1, 2, 3)слід 17, і магічні числа, ймовірно, не досягнуть цієї мети.
Джо Вайт

24
const int TWO = 2;навіть гірше, ніж просто використовувати 2. Це відповідає формулюванню правила з наміром порушити його дух.
Agent_L

4
Що таке число, яке "нічого не означає"? Чому це було б у вашому коді, якщо це нічого не означало?
Тім Грант

6
Звичайно. Залиште коментар перед серією таких тестів, наприклад, "невеликий вибір прикладів, визначених вручну". Це буде відносно інших ваших тестів, які чітко випробовують межі та винятки.
davidbak

5
Ваш приклад вводить в оману - коли ім’я вашої функції було б насправді foo, це нічого не означало б, і тому параметри. Але насправді, я впевнений , що функція не має це ім'я, і ці параметри не мають назви bar1, bar2і bar3. Зробіть більш реалістичний приклад, коли імена мають значення, тоді має набагато більше сенсу обговорювати, чи потрібні імена даних тестових даних.
Doc Brown

Відповіді:


81

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

Зазвичай, коли числа мають якесь значення, вам слід призначити їх локальним змінним методу тестування, щоб зробити код більш читабельним і зрозумілим. Імена змінних повинні принаймні відображати, що означає змінна, а не обов'язково її значення.

Приклад:

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для позначення сенсу змінної, але не для того, щоб її значення було якимось особливим.


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, на вашу мову вибору, можливо, є кращим способом досягнення ваших цілей, залишаючись зрозумілим для інших.


11
Випадкове тестування може виявити помилок, які важко відтворити, але тестування випадковим чином майже не знаходить відтворюваних помилок. Не забудьте зафіксувати будь-які помилки тесту за допомогою певного відтворюваного тесту.
JBRWilkinson

5
І звідки ви знаєте, що ваш тест одиниці не помиляється, коли ви «запевняєте щось про результат» (у цьому випадку перераховуйте, що fooобчислює) ...? Якби ви були на 100% впевнені, що ваш код дає правильну відповідь, тоді ви просто поставите цей код у програму, а не перевіряйте його. Якщо вас немає, то вам потрібно пройти тест, і я думаю, кожен бачить, куди це йде.

2
Так, якщо ви передаєте випадкові входи у функцію, ви повинні знати, яким буде вихід, щоб мати можливість стверджувати, що вона працює правильно. З фіксованими / обраними тестовими значеннями ви, звичайно, можете обробити їх вручну тощо. Але, безумовно, будь-який автоматизований метод детекції, якщо результат правильний, зазнає таких же проблем, як і функція, яку ви тестуєте. Ви або використовуєте наявну у вас реалізацію (яку ви не можете, тому що ви перевіряєте, чи працює вона), або ви пишете нову реалізацію, яка так само є ймовірною помилкою (або більше того, що ви скористаєтеся більшою ймовірністю правильної ).
Кріс

7
@NajibIdrissi - не обов’язково. Наприклад, ви можете перевірити, що застосування зворотної операції, яку ви тестуєте, до результату повертає початкове значення, з якого ви почали. Або ви можете протестувати очікуваних інваріантів (наприклад, для всіх розрахунків відсотків за dднями, розрахунок у dднях + 1 місяць має бути відомим відсотковою ставкою щомісячно вище) тощо.
Жюль

12
@Chris - У багатьох випадках перевірити правильність результатів простіше, ніж генерувати результати. Хоча це неправда в будь- яких обставинах, є багато, де це є. Приклад: додавання запису до врівноваженого бінарного дерева повинно призвести до нового дерева, яке також є збалансованим ... просте тестування, досить складне втілення на практиці.
Жуль

11

Назва вашого тесту на одиницю повинна містити більшу частину контексту. Не від значень констант. Назва / документація для тесту повинна містити відповідний контекст та пояснення будь-яких магічних чисел, присутніх у тесті.

Якщо цього недостатньо, невеликий фрагмент документації повинен бути в змозі надати його (чи то через ім'я змінної, так і через docstring). Майте на увазі, що сама функція має параметри, які, сподіваємось, мають значущі імена. Скопіювати їх у свій тест, щоб назвати аргументи, досить безглуздо.

І останнє: якщо ваші тестові випробування досить складні, що це важко / не практично, ви, ймовірно, маєте занадто складні функції і можете подумати, чому це так.

Чим неохайніше ви пишете тести, тим гірше буде ваш фактичний код. Якщо ви відчуваєте необхідність називати свої тестові значення, щоб зробити тест зрозумілим, це настійно говорить про те, що ваш фактичний метод потребує кращого іменування та / або документації. Якщо ви знайдете необхідність називати константи в тестах, я б розібрався, навіщо вам це потрібно - ймовірно, проблема полягає не в самому тесті, а в реалізації


Ця відповідь, мабуть, стосується складності підрахунку мети тесту, тоді як власне питання стосується магічних чисел у параметрах методу ...
Роббі Ді

@RobbieDee ім'я / документація для тесту має містити відповідний контекст та пояснення будь-яких магічних чисел, присутніх у тесті. Якщо ні, то додайте документацію або перейменуйте тест, щоб бути більш зрозумілим.
Ендерланд

Ще було б краще дати імена чарівних чисел. Якщо кількість параметрів слід змінити, документація ризикує застаріти.
Роббі Ді

1
@RobbieDee майте на увазі, що сама функція має параметри, які, сподіваємось, мають значущі імена. Скопіювати їх у свій тест, щоб назвати аргументи, досить безглуздо.
Ендерленд

"Сподіваюся" так? Чому б просто не кодувати річ належним чином і не усунути те, що нібито магічне число, як Філіп вже окреслив ...
Роббі Ді

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коли запитуєте, як поводитися зі значенням параметрів, мабуть, не найкраща ідея.


Розумне рішення.
користувач1725145

6

Чому ми хочемо використовувати названі константи замість чисел?

  1. DRY - Якщо мені потрібно значення в 3 місцях, я хочу його визначити лише один раз, тому я можу змінити його в одному місці, якщо воно зміниться.
  2. Дайте значення цифрам.

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

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 )

Або скористайтеся тестовим фреймворком, який дозволить вам визначити тестові випадки в якомусь масиві або форматі Map:

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

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повинні бути замінені значущими іменами, про які йшлося у відповіді Філіпа .

(Тут спірний загальний принцип: Числа - це не завжди "магічні числа", для яких потрібні імена; натомість цифри можуть бути даними . Якщо було б сенс помістити свої номери в масив, можливо, масив записів, то, ймовірно, це дані . І навпаки, якщо ви підозрюєте, що у вас можуть бути дані на руках, подумайте про те, щоб ввести їх у масив та придбати їх більше.)


1

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

Якщо тест триває 5 рядків і дотримується основної схеми заданої / коли / тоді, вилучення таких значень в константи лише зробить код довшим і важче читати. Якщо логіка "Коли я додаю користувача на ім'я Сміт, я бачу користувача, який Сміт повертається у списку користувачів", немає сенсу витягувати "Сміта" в константу.

Це звичайно застосовується, якщо ви можете легко зіставити значення, використані в блоці "заданий" (налаштування), з тими, які знаходяться в блоках "коли" і "тоді". Якщо ваша тестова установка відокремлена (у коді) від місця використання даних, можливо, краще використовувати константи. Але оскільки тести найкращі самостійні, налаштування зазвичай близьке до місця використання і застосовується перший випадок, тобто магічні константи в цьому випадку цілком прийнятні.


1

По-перше, давайте погодимось, що "тестова одиниця" часто використовується для покриття всіх автоматизованих тестів, які пише програміст, і що безглуздо обговорювати, як слід називати кожен тест ...

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

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

У нас було багато тестів, які робили вище, і перевіряли, чи були результати однакові, це говорило нам, що ми не змінили те, що ця частина системи зробила помилково під час рефакторингу і т. Д. Це нічого не сказало нам про правильність що зробила та частина системи.

Ці тести були дорогими для підтримання, оскільки будь-яка зміна оптимізуючого коду порушила б тести, але вони також виявили деякі помилки в набагато більшому коді, який попередньо обробляв дані та обробляв результати.

Коли ми «глузували» з бази даних, ви могли назвати ці тести «одиничними тестами», але «одиниця» була досить великою.

Часто, коли ви працюєте в системі без тестів, ви робите щось подібне до вищезгаданого, щоб ви могли підтвердити, що рефакторинг не змінює вихід; сподіваємось, кращі тести написані для нового коду!


1

Я думаю, що в цьому випадку числа слід називати довільними числами, а не магічними числами, а просто коментувати рядок як "довільний тестовий випадок".

Звичайно, деякі Чарівні числа можуть бути і довільними, що стосується унікальних значень "ручки" (які, звичайно, слід замінити названими константами), але також можуть бути заздалегідь обчислені константи на кшталт "повітряної швидкості незареєстрованого європейського горобця у фургонах на дві тижні", де числове значення підключено без коментарів чи корисного контексту.


0

Я не буду ризикувати, якщо сказати остаточний «так / ні», але ось кілька питань, які ви повинні задати собі, вирішуючи, добре це чи ні.

  1. Якщо цифри нічого не означають, чому вони там в першу чергу? Чи можна їх замінити чимось іншим? Чи можете ви робити перевірку на основі викликів методу та потоку замість тверджень про значення? Розглянемо щось на зразок verify()методу Mockito, який перевіряє, чи були зроблені певні виклики методів для знущання над об'єктами, а не насправді ствердженням значення.

  2. Якщо номери робити що - то це означає, то вони повинні бути віднесені до змінних, які названі відповідним чином .

  3. Написання числа 2, яке TWOможе бути корисним у певних контекстах, а не в інших контекстах.

    • Наприклад: assertEquals(TWO, half_of(FOUR))має сенс для того, хто читає код. Відразу зрозуміло, що ви тестуєте.
    • Однак , якщо ваш тест assertEquals(numCustomersInBank(BANK_1), TWO), то це не робить цього особливого сенсу. Чому ж BANK_1містить два клієнта? Для чого ми тестуємо?
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.