Найти в Дзене

Python. Введение в тестирование часть 2

Оглавление

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

а мы продолжаем погружаться в мир тестирования
а мы продолжаем погружаться в мир тестирования

__________________________________________________________________________________________

Выбор приложения для запуска тестов

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

- unittest

- nose/nose2

- pytest

При выборе важно учитывать, как свой уровень подготовки, так и соответствие инструментов тестирования требованиям проекта.

unittest

Unittest была встроена в стандартные библиотеки Python начиная с версии 2.1. Использование этой библиотеки можно найти в коммерческих проектах и в открытом коде.

Unittest содержит как тестовый фреймворк так и приложение для запуска тестов. Так же у этой библиотеки есть определенные требования к написанию и запуску тестов.

Необходимо чтобы:

- ваши тесты были записаны как методы класса

- для подтверждения вы использовали определенные методы встроенные в библиотеку unittest а не стандартный метод assert для Python

Чтобы переписать наш пример для unittest, необходимо:

- импортировать библиотеку unittest

- создать класс с именем TestSum, который будет наследником TestCase класса

- преобразовать тестовую функцию в метод класса. Для этого необходимо добавить self в качестве первого аргумента

- изменить метод подтверждения на sellf.assertEqual().

- изменить точку входа командной строки для вызова unittest.main()

Следуя инструкции, создайте новый файл test_sum_unittest.py и внесите в него следующий код:

-2

Если вы запустите этот скрипт в командной строке, то получите в ответ один успех (обозначен .) и один провал (обозначен F)

-3

Вы только что выполнили 2 теста с помощью unittest.

Заметка: в Python версии 2 и 3 используются разные библиотеки unittest. Так в Python версий 2.7 и ниже название библиотеки — unittest2. Если вы просто импортируете unittest, то получите разный результат в разных версиях Python.

Для большей информации вы можете прочитать документацию unittest.

nose

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

nose совместим со всеми тестами, написанными с помощью unittest фреймворка и позволяет заменить собой приложение для запуска тестов библиотеки unittest. В какой-то момент разработка nose начала отставать от основного потока, но, поскольку код приложения открытый, появилось ответвление nose2. Если вы только начинаете, рекомендуем вам сразу использовать nose2.

Чтобы начать, установите библиотеку nose2 и запустите её в командной строке. nose2 попытается найти все тестовые скрипты под названием test*.py и унаследовать все тестовые кейсы из unittest.TestCase в текущей директории.

-4

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

pytest

Pytest так же поддерживает исполнение тестовых кейсов unittest, но настоящие преимущества этой библиотеки вы поймете, когда начнете писать тестовые кейсы на pytest. Тестовые кейсы в pytest — это серия функций в Python файле, которые начинаются с имени test_.

Преимущества pytest:

- поддержка встроенного оператора assert вместо использования специальных методов вида self.assert*()

- поддержка фильтрации тестовых кейсов

- возможность перезапуска с последнего проваленного теста

- в библиотеку включена целая экосистема из сотен плагинов, которые позволяют расширить функциональность

Уже известный нам кейс TestSum написанный с помощью pytest:

-5

Вы можете отказаться от TestCase, любых дополнительных классов и командной строки для ввода команд.

Дополнительную информацию можно найти на сайте с документацией Pytest.

Пишем свой первый тест

Давайте соберем вместе все то, что вы узнали на данный момент и , вместо тестирования встроенной функции sum(), протестируем простую самописную реализацию того же функционала.

Создайте новую директорию для проекта и еще одну директорию под названием my_sum внутри неё. Внутри my_sum необходимо создать пустой файл __init__.py. Такая структура означает, что директория my_sum может быть импортирована в качестве модуля из родительской директории.

Сейчас ваш проект выглядит примерно так:

project/

└── my_sum/
└── __init__.py

Откройте my_sum/__init__.py и создайте новую функцию с названием sum(), которая получает на вход итерируемый объект (список, кортеж или множество) и складывает его значения вместе:

-6

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

Где мы пишем тесты

Перед тем как начать писать тесты нужно создать файл test.py, который и будет содержать ваш первый тестовый кейс. Поскольку файл должен быть импортируемым, вам стоит поместить его в корневую директорию проекта. Теперь дерево проекта выглядит примерно так:

project/

├── my_sum/
│ └── __init__.py
|
└── test.py

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

Заметка: Что если все ваше приложение это всего лишь один скрипт?

Вы можете импортировать любые атрибуты скрипта (например классы, функции и переменные) с помощью встроенной функции __import__(). Вместо from my_sum import sum вы можете написать:

target = __import__(«my_sum.py»)

sum = target.sum

Этот прием позволяет не добавлять целый пакет в наш скрипт, а ограничиться его частью. Так же этот функционал пригодится, если имя вашего файла совпадает с какой-либо стандартной библиотекой. Например math.py совпадает с модулем math

Структура простого теста

Перед тем как вы уже приступите к написанию тестов, необходимо ответить на несколько вопросов:

- Что именно вы хотите протестировать?

- Это юнит или интеграционный тест?

Затем нужно просто следовать инструкциям:

- создайте входящие значения

- выполните тестируемый код, сохраните результат

- сопоставьте результат тестов с ожидаемым результатом.

В нашем приложении sum() есть большое количество вопросов, которые надо протестировать:

- Сможет ли sum() выдать сумму списка, состоящего из цифр (integers)?

- Сможет ли sum() суммировать кортеж или множество?

- Сможет ли sum() суммировать список дробных чисел (floats)?

- Что случится, если вы подадите на вход одну цифру или строку?

- Что случится если одно значение из списка будет отрицательным?

Проще всего проверить список числе (integer). Создаем файл test.py, в который запишем следующий код:

-7

В этом примере кода:

- импортируем sum() из созданного нами пакета my_sum

- определяем класс тестовых кейсов под названием TestSum, который является наследником unittest.TestCase

- определяем тестовый метод test_list_int(), чтобы проверить как наша функция будет взаимодействовать со списком, состоящим из чисел:

- определяем переменную data, содержащую список чисел [1,2,3]

- сохраняем результат функции в переменную result

- подтверждаем, что значение result совпадает с ожидаемым значением 6, с помощью метода .assertEqual() класса unittest.TestCase

- определяем запуск программы из под командной строки, который запускает приложение для запуска тестов unittest .main()

Если вы не уверены, что такое self или как определяется .assertEqual(), вы можете освежить свои знания объектно-ориентированного программирования Python 3 Object-Oriented Programming.

Как писать подтверждения (assertions)

Последний шаг написания тестов — валидация ответа теста на основании ожидаемого ответа. Этот процесс называют подтверждением (assertion). Есть несколько общих практик, помогающих писать подтверждения:

- убедитесь, что тесты воспроизводимы и если вы запускаете тесты много раз, то получаете один и тот же результат

- попробуйте подтверждать результаты, которые основаны на ваших входных данных. Как мы делали с суммой и функцией sum()

unittest содержит множество методов, которые помогут вам подтвердить значение, типы и существование переменных. Вот список наиболее часто используемых методов:

-8

.assertIs(), .assertIsNone(), .assertIn(), и .assertIsInstance() обладают противоположными методами .assertIsNot() и т.д.

Побочные эффекты

Часто необходимо протестировать вещи более сложные чем значение, возвращаемое функцией. Выполнение части кода может менять другие объекты в окружении. Например атрибуты классов или файлы в файловой системе, значение в базе данных. Эти явления называются побочный эффект (side effect) и являются важной частью тестирования. Определите, какой из побочных эффектов вы будете тестировать перед тем, как включить его в свой лист подтверждений.

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

Выполнение вашего первого теста

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

Приложения запускающие тесты

Python приложение, которое выполняет ваш тестовый код, проверяет подтверждения и возвращает вам в консоль результаты тестирования называется приложением для запуска тестов (test runner).

Внизу test.py вы добавили небольшой сниппет:

-9

Этот код означает, что если вы выполните скрипт отдельно запустив python test.py в командной строке, то будет вызван unittest.main(). Это запустит приложение для запуска тестов, которое найдет все классы-наследники unittest.TestCase и выполнит их.

Это только один из множества путей вызвать приложение для запуска тестов unittest. В случае если у вас только один файл содержащий тестовый код, запуск напрямую с помощью python test.py — отличный способ начать.

$ python -m unittest test

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

-10

Мы так же выполнили тест внутри test.py и вывели результаты в консоль. Режим подробного отчета позволяет сначала выводить имена и результат выполнения каждого теста.

Вместо указания на конкретные тестовые модули или директории вы можете запросить автопоиск тестовых файлов:

$ python -m unittest discover

Эта команда запускает в текущей директории поиск всех файлов, которые названы test*.py и попытки их запустить.

Как только у вас появилось несколько тестовых файлов, пока вы следуете правилу правильных имен (test*.py) вы можете указывать для автопоиска имя директории, содержащей тестовые файлы с помощью флага -s.

$ python -m unittest discover -s tests

unittest запустит все тесты в одном тестовом плане и вернет вам результаты.

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

$ python -m unittest discover -s tests -t src

unittest просканирует директорию tests на наличие тестов и выполнит их из директории src/

Какую информацию возвращают тесты?

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

Sum() должна быть способна работать с другими числовыми типами. Например с дробями.

Импортируем тип Fraction из стандартного библиотечного модуля fractions в нашем файле test.py:

from fractions import Fraction

Теперь добавим тесты, который ожидает некорректное значение. В нашем случае это будет сумма 1 при сложении ¼, ¼ и 2/5.

-11

Когда вы исполните тест с помощью python -m unittest test, получите следующий ответ:

-12

В ответе вы увидите следующую информацию:

- первая строка показывает результат исполнения тестов. Один проваленный (F) и один успешный (.).

- заголовок FAIL показывает детали проваленного теста:

- имя метода (test_list_fraction)

- имя модуля (test) и тестового кейса (TestSum)

- дальше идет несколько пояснений по конкретному тесту. Ожидаемый результат тестирования и полученный.

Помните, что вы всегда можете получить больше информации добавив флаг -v в команду python -m unittest.

__________________________________________________________________________________________

На этом вторая часть перевода завершена. Дальше будет немного о тестировании в фреймворках Django и Flask, некоторые продвинутые сценарии тестирования, интеграционные тесты и много крутых штук) Как любят говорить на ютубе подписывайтесь на канал, ставьте лайк, а я буду стараться чаще выкладывать переводы и писать свои заметки.

Наука
7 млн интересуются