Урок №76. Введення в тестування коду

  Юрій  | 

  Оновл. 28 Чер 2020  | 

 74

Отже, ви написали програму, вона компілюється, і навіть працює! Що далі? Є декілька варіантів.

Навіщо виконувати тестування?

Якщо ви написали програму, щоб її один раз запустити і забути, то далі нічого робити не потрібно. Тут не так важливо, що ваша програма може некоректно функціонувати в певних ситуаціях. Якщо при першому запуску вона працює, так як ви і очікували, і якщо ви далі запускати і використовувати її не плануєте, тоді все — фініш.

Якщо ваша програма повністю лінійна (не має умовного розгалуження: операторів if/else або switch), не приймає вхідних даних і виводить правильний результат, тоді також фініш. В цьому випадку ви вже протестували всю програму, запустивши її один раз і звіривши результат.

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

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

Тестування програмного забезпечення — це процес визначення працездатності програмного забезпечення відповідно до очікувань розробника.

Перш ніж ми будемо говорити про деякі практичні способи тестування вашого коду, давайте поговоримо про те, чому комплексне тестування може бути складним. Наприклад, розглянемо наступну програму:

Враховуючи 4-х байтовий тип int і його діапазон значень, для тестування всіх можливих значень нам потрібно буде 18 446 744 073 709 551 616 (~ 18 квінтильйонів) разів запустити цю програму. Зрозуміло, що це абсурд.

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

Зараз ваша інтуїція повинна підказувати вам, що для того, щоб переконатися в повній працездатності вищенаведеної програми не потрібно буде її запускати 18 квінтильйонів разів. Ви можете прийти до висновку, що якщо код виконується, коли вираз x > y дорівнює true при одній парі значень x і y, то код повинен коректно працювати і з будь-якими іншими парами x і y, де x > y. З огляду на це, стає очевидним, що для тестування програми нам потрібно запустити її всього лише три рази (по одному для кожного випадку: x > y, x < y і x = y), щоб переконатися, що вона працює коректно. Є й інші трюки, які дозволяють спростити процес тестування коду.

Про методології тестування можна довго розповідати, але так як ця тема не дуже специфічна для C++, то ми будемо дотримуватися короткого і зрозумілого введення, охопленого з точки зору вас як розробника, який тестує власний код.

Виконання неофіційного тестування

Більшість розробників проводять неофіційне тестування, коли пишуть свої програми. Після написання частини коду (функції, класу або будь-якого іншого “шматка коду”) розробник пише деякий код для перевірки щойно доданої частини, і, якщо тест пройдено успішно, розробник видаляє код цього тесту. Наприклад, для наступної функції isLowerVowel() ми можемо написати наступний код для перевірки:

Якщо ви отримаєте 1 або 0, то тоді все добре. Ви знаєте, що ваша функція працює, тому можна видалити тимчасовий тестовий код і продовжити процес програмування.

Порада №1: Пишіть свою програму по частинам: в невеликих, чітко визначених одиницях (функціях)

Візьмемо, наприклад, автовиробника, який створює автомобіль. Як ви думаєте, який з варіантів нижче він використає?

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

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

Чи не здається вам, що більш правильним є перший варіант? І все ж більшість початківців пишуть свій код відповідно до другого варіанту!

У другому випадку, якщо будь-яка з частин автомобіля буде працювати неправильно, то механіку доведеться провести діагностику всього автомобіля, щоб визначити, що пішло не так — проблема може бути де завгодно. Наприклад, автомобіль може не заводитися через несправну свічку запалювання, акумулятор, паливний насос або через ще щось. Це призведе до великої кількості марно витраченого часу в спробах точного визначення кореня проблеми. І, якщо проблема буде знайдена, наслідки можуть бути катастрофічними: зміна в одній частині автомобіля може привести до «ефекту метелика» — серйозним змінам в інших частинах автомобіля. Наприклад, занадто маленький паливний насос може привести до зміни двигуна, що призведе до реорганізації каркасу автомобіля. В кінцевому підсумку вам доведеться переробляти більшу частину авто, просто щоб виправити те, що спочатку було невеликою проблемою!

У першому випадку автовиробник перевіряє всі деталі по мірі надходження. Якщо будь-який з компонентів виявився бракованим, то механіки відразу розуміють, в чому проблема і як її вирішити. Ніщо не інтегрується в автомобіль, поки не буде успішно протестовано. На той час, коли вони вже зберуть весь автомобіль, у них буде розумна впевненість в його працездатності — врешті-решт, всі його частини були успішно протестовані. Все ж залишається ймовірність, що щось може піти не так при з’єднанні всіх частин, але в порівнянні з другим варіантом — це дуже мала ймовірність, про яку і не слід серйозно турбуватися.

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

Правило: Часто компілюйте свій код і завжди тестуйте всі нетривіальні функції, які ви написали.

Порада №2: Націлюйтесь на 100% покриття коду

Термін “покриття коду” відноситься до кількості вихідного коду програми, який був задіяний під час тестування. Є багато різних показників покриття коду, але ми розглянемо лише основні.

Покриття стейтментів — це відсоток стейтментів в вашому коді, які були задіяні під час виконання тестування. Наприклад:

Виклик boo(1, 0) дасть вам повне охоплення стейтментів цієї функції, так як виконається кожен рядок коду.

У випадку з функцією isLowerVowel():

Тут знадобиться два виклики для перевірки всіх стейтментів, так як визначити роботу стейтментів №2 і №3 в одному виклиці функції ми не зможемо.

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

Порада №3: Націлюйтесь на 100% покриття розгалужень

Термін “покриття розгалужень” відноситься до відсотку розгалужень, які були виконані в кожному випадку (позитивному і негативному) окремо. Оператор if має два розгалуження: випадок true і випадок false (навіть якщо немає оператора else). Оператор switch може мати багато розгалужень. Наприклад:

Попередній виклик boo(1, 0) дав нам 100% охоплення стейтментів і випадок true. Але це всього лише 50% охоплення розгалужень. Нам потрібен ще один виклик — boo(0, 1), щоб протестувати випадок false.

У функції isLowerVowel() потрібні два виклики (наприклад, isLowerVowel('a') і isLowerVowel('q')), щоб переконатися в 100% охопленні розгалужень (всі літери, які знаходяться в switch тестувати не обов’язково, якщо спрацювала одна — спрацюють і інші):

Переглянемо функцію порівняння з прикладу вище:

Тут необхідні 3 виклики функції, щоб отримати 100% охоплення розгалужень:

   compare(1,0) перевіряє варіант true для першого оператора if.

   compare(0, 1) перевіряє варіант false для першого оператора if і варіант true для другого оператора if (else if).

   compare(0, 0) перевіряє варіант false для другого оператора if і виконує інструкцію else.

Таким чином, ми можемо сказати, що цю функцію можна протестувати за допомогою всього лише 3-х викликів функції (а не 18 квінтильйонів).

Правило: Тестуйте кожен випадок розгалуження в вашій програмі.

Порада №4: Націлюйтесь на 100% покриття циклів

Покриття циклів (або ще неофіційно «тест 0, 1, 2») означає те, що якщо у вас в коді є цикл, то, щоб переконатися в його працездатності, потрібно його виконати 0, 1 і 2 рази. Якщо він працює коректно в другій ітерації, то повинен працювати коректно і для всіх наступних ітерацій > 2 (3, 4, 10, 100 і т.д.). Наприклад:

Щоб протестувати цикл всередині функції, нам доведеться викликати його три рази:

   spam(0) для перевірки випадку нульової ітерації.

   spam(1) для перевірки ітерації №1 і spam(2) для перевірки ітерації №2.

   Якщо spam(2) працює, тоді і spam(n) буде працювати (де n > 2).

Правило: Використовуйте “тест 0, 1, 2” для перевірки працездатності циклів з різною кількістю ітерацій.

Порада №5: Переконайтеся, що ви тестуєте різні типи вводу

Коли ви пишете функції, які приймають параметри або користувацький ввід, то подивіться, що відбувається з різними типами вводу. Наприклад, якщо я написав функцію обчислення квадратного кореню з цілого числа, то які значення потрібно було б протестувати? Найімовірніше, ви почали б зі звичайних значень, наприклад, з 4. Але також було б непогано протестувати і 0, і яке-небудь від’ємне число.

Ось декілька основних рекомендацій щодо тестування різних типів вводу:

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

   Для чисел типу з плаваючою крапкою переконайтеся, що ви розглянули варіанти, як ваша функція обробляє значення, які мають неточності (значення, які трохи більше/менше очікуваних). Хороші тестові значення — це 0.1 і -0.1 (для перевірки чисел, які трохи більше очікуваних) і 0.6 і -0.6 (для перевірки чисел, які трохи менше очікуваних.

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

Правило: Тестуйте різни типи вводу, щоб переконатися, що ваш “шматок коду” правильно їх оброблює.

Зберігання ваших тестів

Хоча написання тестів і подальше їх видалення — досить-таки хороший варіант для швидкого та тимчасового тестування, але для коду, який ви маєте намір повторно використовувати або модифікувати в майбутньому, є сенс в зберіганні цих тестів. Наприклад, замість видалення вашого тимчасового тесту, ви можете перемістити його в функцію test():

Автоматизація тестування

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

Тепер ви можете викликати test() в будь-який час і функція все зробить за вас.

Тест

Завдання №1

Коли ви повинні починати тестувати свій код?

Відповідь №1

Відразу, як тільки написали нетривіальну функцію.

Завдання №2

Скільки тестів потрібно написати для наступної функції для мінімального підтвердження її працездатності?

Відповідь №2

Чотирьох тестів буде достатньо:

   Один для перевірки випадків a/e/i/o/u.

   Один для перевірки випадку за замовчуванням.

   Один для тестування isLowerVowel('y', true).

   Один для тестування isLowerVowel('y', false).

Оцінити статтю:

1 Зірка2 Зірки3 Зірки4 Зірки5 Зірок (2 оцінок, середня: 5,00 з 5)
Loading...

Залишити відповідь

Ваш E-mail не буде опублікований. Обов'язкові поля відмічені *