Тестування в Python – об’єктно-орієнтована і процедурний підхід (исходники), Різне, Програмування, статті

Введення


Тестування – головний біль для будь-якого розробника. Кожен (чи майже кожен) готовий погодитися з тим, що тестування необхідно, і абсолютно у кожного є парочка “поважних причин”, щоб не писати тести. В компільованих мовах зі статичною типізацією (наприклад, C + +) частину роботи з перевірки коректності коду “бере на себе” компілятор; концентрованим виразом ідеї “мови, на якому не можна написати помилковий код “стала мова Ада – прямо скажемо, не найпопулярніший серед програмістів. У динамічних мовах, таких, як Python або Perl на етапі компіляції відбувається сама мінімальна перевірка вихідного коду, тому виникає необхідність (на радість адептам горезвісної методології Test Driven Development “розробка через тестування”) тестувати буквально кожну строчку.


На щастя, творці динамічних мов програмування зрозуміли це досить швидко, і стали включати в свої стандартні бібліотеки засоби тестування. У мові Python використовуються відразу дві системи тестування: doctest і unittest; doctest – річ досить специфічна, і, хоча у неї є свої переваги і свої прихильники, її розгляд виходить за рамки цієї статті. Інфраструктура unittest (Її вихідний код знаходиться в модулі unittest стандартної бібліотеки Python) набагато звичніше, особливо для тих, хто вже мав справу з JUnit, DUnit та іншими xUnit.


unittest і funtest


Розглянемо невеликий приклад використання unittest. Ті, кому не до душі об’єктно-орієнтоване програмування – а такі ще зустрічаються, причому не тільки між закостенілих “динозаврів” часів Алгол, і серед них, до речі, трапляються непогані фахівці – можуть його пропустити.





import unittest
from unittest import TestCase, main
import my_math
from my_math import factorial
class FactorialTestCase(TestCase):
def test_fact_0(self):
“””test factorial(0)”””
self.assertEquals(factorial(0), 1)
def test_fact_1(self):
“””test factorial(1)”””
self.assertEquals(factorial(1), 1)
def test_fact_2(self):
“””test factorial(2)”””
self.assertEquals(factorial(2), 2)
def test_fact_3(self):
“””test factorial(3)”””
self.assertEquals(factorial(3), 6)
if __name__ == “__main__”:
main()

У цьому прикладі ми тестуємо функцію factorial, яку імпортуємо з деякого модуля my_math. Для тестування ми імпортуємо з модуля unittest клас TestCase, створюємо похідний від нього клас FactorialTestCase, визначаємо в класі FactorialTestCase кілька тестів. Кожен тест – це метод з єдиним аргументом self, ім’я методу обов’язково повинно починатися з префікса test (цілком логічне угоду про найменуваннях). Далі ми викликаємо визначену в модулі unittest функцію main, яка знаходить створений нами клас, в ньому знаходить всі тести (тобто всі методи, імена яких починаються з приставки “test”), створює стільки примірників класу, скільки в ньому визначено таких методів, і для кожного екземпляра класу викликає один і тільки один з цих методів. У цьому прикладі функція main створює чотири примірники класу FactorialTestCase. Для одного буде викликаний метод test_fact_0, для іншого – test_fact_1 і т.д. Якщо в ході проходження будь-якого тесту виникає виняток, то такий тест вважається неуспішним. При виконанні функції main всі виключення перехоплюються, неуспішні тести реєструються, а по закінченні тестування на екран виводиться звіт, в якому вказується, скільки тестів було проведено, і які з них виявилися неуспішними. Метод assertEquals успадкований класом FactorialTestCase від свого “батька” – класу TestCase, і всього лише перевіряє, чи рівні між собою дві величини (в даному випадку – результат, що повертається функцією factorial, і очікуване нами значення результату). Якщо вони не рівні, метод assertEquals генерує виняток AssertionError; як уже говорилося, при виконанні функції main, все винятку виникають в ході тестування перехоплюються, а після закінчення тестування, видається звіт про виниклі проблеми.


Необхідно ще сказати пару слів про коментарі (точніше кажучи, рядках документації, в англомовній літературі з Python званих docstrings) – це той самий текст в кумедних “потрійних” лапках, який знаходиться між заголовком методу і його тілом. В unittest цей текст виводиться в звіті про помилки для того, щоб повідомити програмісту, які саме тести були неуспішними. Для тіста, у якого рядок документації відсутній, буде виведено ім’я тесту, що вже не так наочно, тому краще не лінуватися писати ці самі docstring. До речі, з цієї ж причини не варто “забивати” в один тест занадто багато різних перевірок – з тим же успіхом можна взагалі відмовитися від використання unittest і писати всі тести у вигляді одного скрипта “в стилі акина”, без розбивки на структурні одиниці. При цьому ми, звичайно, отримаємо інформацію про те, що при тестуванні щось пішло не так, але от знайти відповідь на питання, де саме відбулася помилка, буде вже набагато важче.


Додатково unittest надає можливість реалізувати метод, “готує” систему до кожного нового тесту (метод setUp), і метод, що виконує звільнення ресурсів (наприклад, видалення тимчасових файлів) після кожного тесту, незалежно від того, успішний тест чи ні (метод tearDown).


Для “чистих” функцій (тобто тих функцій, які не мають сторонніх ефектів, наприклад, математичних функцій) це різноманіття здається зайвим, а ось код виходить досить об’ємний – плата за універсальність і об’єктно-орієнтований підхід. Ця обставина і спонукала мене написати – ні, не нову інфраструктуру, всього лише процедурно-орієнтований інтерфейс до unittest, “заточений” для роботи з “чистими” функціями. Зрозумійте мене правильно, об’єктно-орієнтоване програмування – річ хороша, але всяка хороша річ хороша на своєму місці.


Ось як буде виглядати попередній приклад при використанні запропонованого процедурно-орієнтованого модуля funtest:





import funtest
from funtest import add_OK_test, do_tests
import my_math
from my_math import factorial
def main()
add_OK_test(factorial, 1, 0)
add_OK_test(factorial, 1, 1)
add_OK_test(factorial, 2, 2)
add_OK_test(factorial, 6, 3)
do_tests()
if __name__ == “__main__”:
main()

Виглядає такий код дещо простіше, ніж попередній, хоча, звичайно, це було б помітніше, якби перед нами був файл, що містить кілька сотень тестів. Втім, простота – поняття відносне, і дехто з адептів об’єктно-орієнтованого програмування, дивлячись на цей код, потисне плечима і скаже: “Черговий велосипед”. Ну й гаразд 😉


Ми імпортуємо з модуля funtest функції add_OK_test і do_tests (там є і інші корисні функції, вони будуть описані трохи далі). Спочатку ми за допомогою функції add_OK_test додаємо тести в деякий набір тестів (набір тестів – о, жах! – реалізований, як статична змінна в модулі funtest; некрасиво, зате відповідає відомій ідіоми “KISS – Keep It Simple, Stupid”). Потім викликаємо функцію do_tests, яка проводить тестування за всіма тестами, які ми додали.


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


Забезпечувати кожен тест описом не потрібно, тому що вихідний код досить простий, а в фінальному звіті про помилки замість рядків документації використовується автоматично створюється опис тесту. Воно містить ім’я тестованої функції (в даному прикладі – factorial), ім’я модуля, в якому ця функція визначена, набір вхідних даних, на якому була виявлена ​​помилка, очікуване і реальне значення результату.


Не дуже красивим виглядає формат виклику функції add_OK_test: спочатку тестуєма функція, потім (увага!) очікуваний результат, і тільки після – передані функції аргументи, через кому. Будь-який нормальний людина волів би, щоб очікуваний результат був у кінці списку аргументів. На жаль, але при виконанні функції add_OK_test, як, втім, і будь-який інший, ми повинні спочатку передати обов’язкові аргументи, і тільки потім – необов’язкові (якщо заглянути у вихідний код модуля funtest, то можна побачити, що у функції add_OK_test довільне число аргументів – механізм, аналогічний недоброї пам’яті параметру “…” в мові C). Звичайно, ми могли б передавати всі аргументи тестованої функції у вигляді однієї змінної (списку чи кортежу), але тоді було б важко використовувати “іменовану” передачу аргументів в стилі Ада і VHDL (механізм досить екзотичний, але іноді дуже корисний):





add_OK_test(some_function, 1, A=0, B=1, C=3)
add_OK_test(some_function, 2, A=0, B=1, C=4)
add_OK_test(some_function, 0, A=1, B=3, C=5)
add_OK_test(some_function, 6, A=3, B=2, C=1)

(В даному прикладі ми вважаємо, що функція some_function приймає три аргументи з іменами A, B і C).


Чи можна додати кілька тестів “за один раз”? Так, можна, але при цьому нам доведеться задовольнятися позиційної передачею аргументів у стилі C / C + +:





add_OK_suite(some_function,
(1, 0, 1, 3),
(2, 0, 1, 4),
(0, 1, 3, 5),
(6, 3, 2, 1))

Функція add_OK_suite (вона теж знаходиться в модулі funtest) додає в набір тестів відразу декілька тестів для однієї тестованої функції. Перший аргумент функції add_OK_suite – тестуєма функція, другий і наступні аргументи – дані для тестів. Дані для кожного тесту ми передаємо у вигляді списку, який починається з очікуваного результату, після якого йдуть аргументи (в порядку їх слідування в списку аргументів на оголошенні тестованої функції).


Функції add_OK_test і add_OK_suite порівнюють результат, що повертається тестованої функцією, з очікуваним значенням “в лоб”, за допомогою оператора “==”, і, якщо оператор “==” поверне False – тест вважається неуспішним. У той же час, в unittest реалізовані методи, якщо можна їх так назвати, порівняння з допуском, коли задаються межі допустимого відхилення результату від очікуваного значення. Чесно кажучи, мені майже не доводилося використовувати цей механізм, і мені не здається, що він дуже вже корисний. Та й допустиме відхилення можна висловити по-різному: задати число значущих цифр (такий метод застосований в unittest), абсолютну похибка, відносну похибка, і т.д. Подібні можливості в модулі funtest не реалізовані, але, якщо знадобиться, ця проблема вирішується досить просто, і навіть без модифікації вихідного коду модуля, з використанням добре відомого патерну Стратегія.


Тести повинні перевіряти не тільки повертається функцією значення. Іноді замість того, щоб повернути результат, функція генерує виключення (наприклад, факторіал визначений тільки на безлічі натуральних чисел, і при спробі обчислити факторіал речового, негативного або комплексного числа буде цілком розумно з боку функції згенерувати виключення ValueError або TypeError). А як перевірити, чи дійсно виникло виняток? Для цієї мети в модулі funtest визначені функції add_E_test і add_E_suit.


Функція add_E_test аналогічна функції add_OK_test, але на відміну від останньої, вона додає в список тестів тест, який перехоплює винятку. У такому тесті передбачається що, якщо викликати тестовану функцію з заданим набором аргументів, то тестуєма функція згенерує виключення заданого типу. Якщо ж виняток не виникло, або тип винятку не той, який очікувався – тест вважається неуспішним. Список аргументів функції add_E_test практично такий же, як список аргументів функції add_OK_test, тільки замість очікуваного значення результату ми вказуємо клас очікуваного винятки:





add_E_test(factorial, ValueError, -1)
add_E_test(factorial, TypeError, 1.1)
add_E_test(factorial, TypeError, 1.1+1j*1.0)

(В даному прикладі ми вважаємо, що спроба обчислити factorial (-1) викличе виключення класу ValueError, а factorial (1.1) – виключення класу TypeError).


Природно, функція add_E_test, як і функція add_OK_test, підтримує як позиційну передачу аргументів тестованої функції в стилі C / C + +, так і передачу “іменованих” аргументів тестованої функції в стилі Ада.


Функція add_E_suite, в свою чергу – прямий аналог функції add_OK_suite, тільки, знову-таки, замість очікуваного результату в списку параметрів ми вказуємо клас очікуваного винятки:





add_E_suite(factorial,
(ValueError, -1),
(TypeError, 1.1),
(TypeError, 1.1+1j*1.0))

Вихідні коди: модуль funtest, тести для модуля funtest і приклад використання


Вихідний код модуля відкритий і загальнодоступний. Ніякого інсталятора не додається – інсталяція річ хороша, але, “всяка хороша річ …” (Див. вище). Щоб провести тестування модуля або подивитися приклад, достатньо просто скопіювати файли з вихідним кодом у тимчасовий робочий каталог. Для “дослідної експлуатації” модуля в складі якого-небудь проекту слід перенести модуль funtest в каталог з вихідними кодами цього проекту (переносити тести модуля або приклад використання необов’язково). Ну і нарешті, щоб встановити модуль “по-дорослому”, у вигляді бібліотечного, потрібно помістити його туди, де лежать інші бібліотечні модулі, і де його зможе знайти будь-яка програма на Python. Для ОС Windows, як правило, призначена для користувача бібліотека розташовується в каталозі, в якому встановлено Python, підкаталог Libsite-packages. Для Linux це звичайно каталог / usr / lib / python / site-packages.






ПОПЕРЕДЖЕННЯ

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


Вихідний код оформлений в стилі, прийнятому в співтоваристві Python, однак подекуди є відступи від цього стилю. По-перше, імена глобальних змінних набрані у верхньому регістрі з поділом символом “підкреслення” (“_”) – Так, мені здається, все ж наочніше. По-друге, в класах, породжених від класу TestCase, імена методів оформлені в стилі Кемел – просто тому, що такий стиль прийнятий в самому модулі unittest. Вихідний код рясно забезпечений коментарями, всі коментарі англійською мовою, і я підозрюю, що вони містять деяку кількість помилок (не смислових, а синтаксичних, природно).


До модулю funtest додаються тести (модулі funtest_tests і funtest_tests_aux), а також додатковий приклад використання модуля funtest – модуль prod.


Для тестування модуля funtest слід запустити з командного рядка модуль funtest_tests (це основний модуль з тестами). Зазначу, що традиційна для Unix рядок із зазначенням шляху до інтерпретатору у вихідних кодах відсутня, тому для запуску слід явно вказати інтерпретатор (наприклад: python. / funtest_tests.py). При цьому ви побачите у вікні консолі щось на кшталт такого:





///////////call do_tests()//////////
…………………….
———————————————————
Ran 21 tests in 0.000s
OK
<><><>do_tests() finished<><><>

Демонстраційний модуль prod містить функцію prod, а також набір тестів для неї, що використовує, природно, модуль funtest. Функція prod перемножує кілька чисел, причому кількість аргументів у неї змінне; її можна викликати з двома і більше числовими аргументами, або з одним аргументом – послідовністю чисел (змінної типу “кортеж” або “список”), в цьому випадку функція поверне твір чисел, що містяться в послідовності.


Для тестування модуля prod слід запустити його з командного рядка, при цьому з’явиться стандартне повідомлення unittest про кількість проведених тестів і кілька повідомлень про помилки – так і повинно бути, в модуль навмисно введені кілька завідомо неуспішних тестів, як приклад.


Деталі реалізації


Нічого революційного модуль funtest, зрозуміло, не пропонує. Все, що в ньому є, засноване на unittest. Для кожного тесту створюється екземпляр класу – нащадка TestCase. Зберігаються вони всі в контейнері – Глобальної змінної, екземплярі класу TestSuite, визначеного в модулі unittest. Функція do_tests проводить тестування, використовуючи методи, визначені в класі TestSuite. У загальних рисах, все практично так само, як і при використанні unittest, за винятком того, що модуль funtest має процедурно-орієнтований інтерфейс і не проводить автоматичний пошук тестів.


У модулі funtest визначений клас – нащадок TestCase, який також називається TestCase (конфлікту не відбувається завдяки тому, що кожен модуль в Python має власний простір імен; щоб уникнути плутанини, ми надалі будемо використовувати його повне ім’я, funtest.TestCase). В цьому класі перевизначений успадкований від предка метод shortDescription (метод shortDescription повертає, як слід з його назви, рядок з описом тесту, він використовується при створенні звіту про результати тестування), і визначений новий метод – runCall. Метод runCall викликає тестовану функцію, передаючи їй необхідні аргументи (набір аргументів і посилання на тестовану функцію передаються конструктору при створенні екземпляра класу і зберігаються в приватних атрибутах екземпляра класу). Результат, що повертається методом runCall – це результат, що повертається тестованої функцією. Відзначу, що метод runCall не намагається перехоплювати винятки, які можуть виникнути при виконанні тестованої функції.


Також визначено два нащадка класу funtest.TestCase: OKTestCase і ExcTestCase. Екземпляри класу OKTestCase створюються функціями add_OK_test і add_OK_suite, а екземпляри класу ExcTestCase – функціями add_E_test і add_E_suite. В обох класах визначено метод runTest – метод, що викликається при проведенні тесту.


У класі OKTestCase метод runTest викликає успадкований від предка метод runCall, і порівнює результат його виконання з очікуваним значенням (яке передається конструктору при створенні екземпляра класу і зберігається в приватному атрибуті екземпляра класу). Якщо вони не рівні, генерується виключення класу AssertionError.


У класі ExcTestCase метод runTest також викликає успадкований від предка метод runCall, і очікує, що виклик runCall призведе до виникнення виключення деякого класу (очікуваний клас виключення передається конструктору при створенні екземпляра класу і зберігається в приватному атрибуті екземпляра класу). Якщо виникне виняток не того класу, що очікується, то воно буде перехоплено не методом runTest, а викликає метод runTest кодом, і інформація про нього потрапить у фінальний звіт про тестування. Якщо виняток не виникне зовсім, то метод runTest згенерує виключення класу AssertionError, яке повідомить про неуспіху тесту. Відзначу, що для перехоплення виключення, що генерується тестованої функцією, використовується метод failUnlessRaises успадкований класом ExcTestCase від свого “дідуся” – класу TestCase, визначеного в модулі unittest.

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*