Хабрахабр

[Перевод] Знакомство с тестированием в Python. Ч.1

Всем доброго!

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

Это руководство для тех, кто уже написал классное приложение на Python, но еще не писал для
них тесты.

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

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

Тестирование Кода

В этом руководстве вы познакомитесь с методами от наиболее простых до продвинутых. Тестировать код можно разными способами.

Ручное Тестирование

Хорошие новости! Автоматизированное vs. Помните, как вы впервые запустили приложение и воспользовались им? Скорее всего вы уже сделали тест, но еще не осознали этого. Такой процесс называется исследовательским тестированием, и он является формой ручного тестирования. Вы проверили функции и поэкспериментировали с ними?

Во время исследовательского тестирования вы исследуете приложение. Исследовательское тестирование — тестирование, которое проводится без плана.

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

Звучит безрадостно, верно?

Автоматическое тестирование — исполнение плана тестирования (части приложения, требующие тестирования, порядок их тестирования и ожидаемые результаты) с помощью скрипта, а не руками человека. Поэтому нужны автоматические тесты. Рассмотрим эти инструменты и библиотеки в нашем туториале. В Python уже есть набор инструментов и библиотек, которые помогут создать автоматизированные тесты для вашего приложения.

Интеграционные Тесты Модульные Тесты VS.

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

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

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

Определяется это с помощью модульного теста. Современные машины сами оповестят вас о поломке лампочек.

Модульный тест помогает изолировать поломку и быстрее устранить ее. Модульный тест (юнит-тест) — небольшой тест, проверяющий корректность работы отдельного компонента.

Мы поговорили о двух видах тестов:

  1. Интеграционный тест, проверяющий компоненты системы и их взаимодействие друг с другом;
  2. Модульный тест, проверяющий отдельный компонент приложения.
  3. Вы можете создать оба теста на Python. Чтобы написать тест для встроенной функции sum(), нужно сравнить выходные данные sum() с известными значениями.

Например, вот так можно проверить что сумма чисел (1, 2, 3) равна 6:

>>> assert sum([1, 2, 3]) == 6, "Should be 6"

Если результат sum() некорректный, будет выдана AssertionError с сообщением “Should be 6” (“Должно быть 6”). Значения правильные, поэтому в REPL ничего не будет выведено. Проверим оператор утверждения еще раз, но теперь с некорректными значениями, чтобы получить AssertionError:

>>> assert sum([1, 1, 1]) == 6, "Should be 6"
Traceback (most recent call last): File "<stdin>", line 1, in <module>
AssertionError: Should be 6

В REPL вы увидете AssertionError, так как значение sum() не равно 6.
Вместо REPL, положите это в новый Python-файл с названием test_sum.py и выполните его снова:

def test_sum(): assert sum([1, 2, 3]) == 6, "Should be 6" if __name__ == "__main__": test_sum() print("Everything passed")

Теперь это можно выполнить в командной строке: Теперь у вас есть написанный тест-кейс (тестовый случай), утверждение и точка входа (командной строки).

$ python test_sum.py
Everything passed

Вы проверили список. Вы видите успешный результат, “Everything passed” (“Все пройдено”).
sum() в Python принимает на вход любой итерируемый в качестве первого аргумента. Создадим новый файл с названием test_sum_2.py со следующим кодом: Попробуем протестировать кортеж.

def test_sum(): assert sum([1, 2, 3]) == 6, "Should be 6" def test_sum_tuple(): assert sum((1, 2, 2)) == 6, "Should be 6" if __name__ == "__main__": test_sum() test_sum_tuple() print("Everything passed")

Выполнив test_sum_2.py, скрипт выдаст ошибку, так как sum() (1, 2, 2) должен быть равен 5, а не 6. В результате скрипт выдает сообщение об ошибке, строку кода и трейсбек:

$ python test_sum_2.py
Traceback (most recent call last): File "test_sum_2.py", line 9, in <module> test_sum_tuple() File "test_sum_2.py", line 5, in test_sum_tuple assert sum((1, 2, 2)) == 6, "Should be 6"
AssertionError: Should be 6

Можно увидеть, как ошибка в коде вызывает ошибку в консоли с информацией, где она произошла, и каким был ожидаемый результат.

На помощь приходят исполнители тестов (test runners). Такие тесты подойдут для простой проверки, но что если ошибки есть больше, чем в одном? Исполнитель тестов — особое приложение, спроектированное для проведение тестов, проверки данных вывода и предоставления инструментов для отладки и диагностики тестов и приложений.

Выбор Исполнителя Тестов

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

  • unittest;
  • nose или nose2;
  • pytest.

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

unittest

1. unittest встроен в стандартную библиотеку Python, начиная с версии 2. При написании и исполнении тестов нужно соблюдать некоторые важные требования. Вы наверняка столкнетесь с ним в коммерческих приложениях Python и проектах с открытым исходным кодом.
В unittest есть тестовый фреймворк и исполнитель тестов.

unittest требует:

  • Помещать тесты в классы, как методы;
  • Использовать специальные методы утверждения. Класс TestCase вместо обычного встроенного выражения assert.

Чтобы превратить ранее написанный пример в тест-кейс unittest, необходимо:

  1. Импортировать unittest из стандартной библиотеки;
  2. Создать класс под названием TestSum, который будет наследовать класс TestCase;
  3. Сконвертировать тестовые функции в методы, добавив self в качестве первого аргумента;
  4. Изменить утверждения, добавив использование self.assertEqual() метода в классе TestCase;
  5. Изменить точку входа в командной строке на вызов unittest.main().

Следуя этим шагам, создайте новый файл test_sum_unittest.py со таким кодом:

import unittest class TestSum(unittest.TestCase): def test_sum(self): self.assertEqual(sum([1, 2, 3]), 6, "Should be 6") def test_sum_tuple(self): self.assertEqual(sum((1, 2, 2)), 6, "Should be 6") if __name__ == '__main__': unittest.main()

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

$ python test_sum_unittest.py
.F
======================================================================
FAIL: test_sum_tuple (__main__.TestSum)
----------------------------------------------------------------------
Traceback (most recent call last): File "test_sum_unittest.py", line 9, in test_sum_tuple self.assertEqual(sum((1, 2, 2)), 6, "Should be 6")
AssertionError: Should be 6 ----------------------------------------------------------------------
Ran 2 tests in 0.001s FAILED (failures=1)

Таким образом, вы выполнили два теста с помощью исполнителя тестов unittest.

В версиях Python 2. Примечание: Если вы пишете тест-кейсы для Python 2 и 3 — будьте осторожны. При импорте из unittest вы получите разные версии с разными функциями в Python 2 и Python 3. 7 и ниже unittest называется unittest 2.

Чтобы узнать больше о unittest’ах почитайте unittest документацию.

nose

Со временем, после написания сотни, а то и тысячи тестов для приложения, становится все сложнее понимать и использовать данные вывода unittest.

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

nose2 попытается найти все тестовые скрипы с test*.py в названии и все тест-кейсы, унаследованные из unittest. Для начала работы с nose2 нужно установить его из PyPl и запустить в командной строке. TestCase в вашей текущей директории:

$ pip install nose2
$ python -m nose2
.F
======================================================================
FAIL: test_sum_tuple (__main__.TestSum)
----------------------------------------------------------------------
Traceback (most recent call last): File "test_sum_unittest.py", line 9, in test_sum_tuple self.assertEqual(sum((1, 2, 2)), 6, "Should be 6")
AssertionError: Should be 6 ----------------------------------------------------------------------
Ran 2 tests in 0.001s FAILED (failures=1)

nose2 предоставляет множество флагов командной строки для фильтрации исполняемых тестов. Так выполняется тест, созданный в test_sum_unittest.py, из исполнителя тестов nose2. Чтобы узнать больше, советуем ознакомиться с документацией Nose 2.

pytest

Но настоящее преимущество pytest — его тест-кейсы. pytest поддерживает выполнение тест-кейсов unittest. Тест-кейсы pytest — серия функций в Python-файле с test_ в начале названия.

Есть в нем и другие полезные функции:

  • Поддержка встроенных выражений assert вместо использования специальных self.assert*() методов;
  • Поддержка фильтрации тест-кейсов;
  • Возможность повторного запуска с последнего проваленного теста;
  • Экосистема из сотен плагинов, расширяющих функциональность.

Пример тест-кейса TestSum для pytest будет выглядеть следующим образом:

def test_sum(): assert sum([1, 2, 3]) == 6, "Should be 6" def test_sum_tuple(): assert sum((1, 2, 2)) == 6, "Should be 6"

Вы избавились от TestCase, использования классов и точек входа командной строки.
Больше информации можно найти на Сайте Документации Pytest.

Написание Первого Теста

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

Внутри my_sum создайте пустой файл с названием _init_.py. Создайте новую папку для проекта, внутри которой создайте новую папку с названием my_sum. Наличие этого файла значит, что папка my_sum может быть импортирована в виде модуля из родительской директории.

Структура папок будет выглядеть так:

project/

└── my_sum/
└── __init__.py

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

def sum(arg): total = 0 for val in arg: total += val return total

Затем, по завершении итерации, результат возвращается. В этом примере создается переменная под названием total, перебираются все значения в arg и добавляются к total.

Где Писать Тест

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

project/

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

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

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

target = __import__("my_sum.py")
sum = target.sum

Это полезно, если имя файла конфликтует с названиями стандартных библиотек пакетов. При использовании __import__() вам не придется превращать папку проекта в пакет, и вы сможете указать имя файла. Например, если math.py конфликтует с math модулем.

Как Структурировать Простой Тест

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

  1. Что вы хотите протестировать?
  2. Вы пишете модульный тест или интеграционный тест?

Для него можно проверить разные поведения, например: Сейчас вы тестируете sum().

  • Можно ли суммировать список целых чисел?
  • Можно ли суммировать кортеж или множество?
  • Можно ли суммировать список чисел с плавающей точкой?
  • Что будет, если дать на вход плохое значение: одно целое число или строку?
  • Что будет, если одно из значений отрицательное?

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

import unittest from my_sum import sum class TestSum(unittest.TestCase): def test_list_int(self): """ Test that it can sum a list of integers """ data = [1, 2, 3] result = sum(data) self.assertEqual(result, 6) if __name__ == '__main__': unittest.main()

Код в этом примере:

  • Импортирует sum() из пакета my_sum(), который вы создали;
  • Определяет новый класс тест-кейса под названием TestSum, наследующий unittest.TestCase;
  • Определяет тестовый метод .test_list_int() для тестирования целочисленного списка. Метод .test_list_int() сделает следующее

:

  1. Объявит переменную data со списком значений (1, 2, 3);
  2. Присвоит значение my_sum.sum(data) переменной result;
  3. Определит, что значение result равно 6 с помощью метода .assertEqual() на unittest.TestCase классе.

  • Определяет точку входа командной строки, которая запускает исполнителя теста unittest .main().

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

Как Писать Утверждения

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

  • Проверьте, что тесты повторяемы и запустите их несколько раз, чтобы убедиться, что каждый раз они дают одни и те же результаты;
  • Проверьте и подтвердите результаты, которые относятся к вашим входным данным — проверьте, что результат действительно является суммой значений в примере sum().

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

Метод

Эквивалент

.assertEqual(a, b)

a == b

.assertTrue(x)

bool(x) is True

.assertFalse(x)

bool(x) is False

.assertIs(a, b)

a is b

.assertIsNone(x)

x is None

.assertIn(a, b)

a in b

.assertIsInstance(a, b)

isinstance(a, b)

У .assertIs(), .assertIsNone(), .assertIn(), and .assertIsInstance() есть противоположные методы, называемые .assertIsNot() и тд.

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

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

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

Запуск Первого Теста

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

Запуск Исполнителей Тестов

В конец test.py добавьте этот небольшой фрагмент кода: Исполнитель тестов — приложение Python, которое выполняет тестовый код, проверяет утверждения и выдает результаты тестирования в консоли.

if __name__ == '__main__': unittest.main()

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

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

Попробуем: Другой способ — использовать командную строку unittest.

$ python -m unittest test

Можно добавить дополнительные параметры для изменения выходных данных. Это исполнит тот же самый тестовый модуль (под названием test) через командную строку. Попробуем следующее: Один из них -v для многословности (verbose).

$ python -m unittest -v test
test_list_int (test.TestSum) ... ok ----------------------------------------------------------------------
Ran 1 tests in 0.000s

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

Вместо предоставления имени модуля, содержащего тесты, можно запросить авто-обнаружение при помощи следующего:


$ python -m unittest discover

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

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

$ python -m unittest discover -s tests

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

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

unittest найдет все файлы test*.py в директории src/ внутри tests, а затем выполнит их.

Понимание Результатов Тестирование

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

sum() должен принимать на вход другие списки числового типа, например дроби.

К началу кода в файле test.py добавьте выражение для импорта типа Fraction из модуля fractions стандартной библиотеки.

from fractions import Fraction

В нашем случае, ожидаем, что сумма ¼, ¼ и ⅖ будет равна 1: Теперь добавим тест с утверждением, ожидая некорректное значение.

import unittest from my_sum import sum class TestSum(unittest.TestCase): def test_list_int(self): """ Test that it can sum a list of integers """ data = [1, 2, 3] result = sum(data) self.assertEqual(result, 6) def test_list_fraction(self): """ Test that it can sum a list of fractions """ data = [Fraction(1, 4), Fraction(1, 4), Fraction(2, 5)] result = sum(data) self.assertEqual(result, 1) if __name__ == '__main__': unittest.main()

Если вы запустите тесты повторно с python -m unittest test, получите следующее:

$ python -m unittest test
F.
======================================================================
FAIL: test_list_fraction (test.TestSum)
----------------------------------------------------------------------
Traceback (most recent call last): File "test.py", line 21, in test_list_fraction self.assertEqual(result, 1)
AssertionError: Fraction(9, 10) != 1 ----------------------------------------------------------------------
Ran 2 tests in 0.001s FAILED (failures=1)

В этих выходных данных вы видите следующее:

  • В первой строке указаны результаты выполнения всех тестов: один проваленный (F), один пройденный (.);
  • FAIL показывает некоторые детали проваленного теста:

  1. Название тестового метода (test_list_fraction);
  2. Тестовый модуль (test) и тест-кейс (TestSum);
  3. Трейсбек строки с ошибкой;
  4. Детали утверждения с ожидаемым результатом (1) и фактическим результатом (Fraction(9, 10))

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

Запуск тестов из PyCharm

Если вы используете PyCharm IDE, то можете запустить unittest или pytest, выполнив следующие шаги:

  1. В окне Project tool, выберите директорию tests.
  2. В контекстном меню выберите команду запуска unittest. Например, ‘Unittests in my Tests…’.

Это выполнит unittest в тестовом окне и выдаст результаты в PyCharm:

Больше информации доступно на сайте PyCharm.

Запуск Тестов из Visual Studio Code

Если вы пользуетесь Microsoft Visual Studio Code IDE, поддержка unittest, nose и pytest уже встроена в плагин Python.

Вы увидите список вариантов: Если он у вас установлен, можно настроить конфигурацию тестов, открыв Command Palette по Ctrl+Shift+P и написав “Python test”.

Кликните по шестеренке для выбора исполнителя тестов (unittest) и домашней директории (.). Выберите Debug All Unit Tests, после чего VSCode отправит запрос для настройки тестового фреймворка.

По завершении настройки, вы увидите статус тестов в нижней части экрана и сможете быстро получить доступ к тестовым логам и повторно запустить тесты, кликнув по иконкам:

Видим, что тесты выполняются, но некоторые из них провалены.

THE END

В следующей части статьи мы рассмотрим тесты для фреймворков, таких как Django и Flask.

Ждём ваши вопросы и комментарии тут и, как всегда, можно зайти к Станиславу на день открытых дверей.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть