Всем привет!
Продолжаем статью о знакомстве с тестированием в Python, которую мы подготовили для вас в рамках нашего курса «Разработчик Python».
Тестирование для Веб-Фреймворков Django и Flask
Если вы пишете тесты для веб-приложений, используя один из популярных фреймворков, например, Django или Flask, то стоит помнить о важных отличиях в написании и запуске таких тестов.
Чем Они Отличаются от Других Приложений
Подумайте о коде, который нужно протестировать в веб-приложении. Все маршруты, представления и модели требуют много импортов и знаний об используемом фреймворке.
Это похоже на тестирование автомобиля, о котором говорили в первой части туториала: перед тем, как провести простые тесты, вроде проверки работы фар, нужно включить компьютер в машине.
Django и Flask упрощают эту задачу и предоставляют тестовый фреймворк на базе unittest. Вы можете продолжать писать тесты привычным образом, но исполнять их чуть иначе.
Как пользоваться исполнителем тестов Django
Шаблон Django startapp создает файл tests.py в каталоге вашего приложения. Если его еще нет, создайте его со следующим содержимым:
from django.test import TestCase
class MyTestCase(TestCase):
# Your test methods
Основное отличие от прошлых примеров — нужно наследовать из django.test.TestCase
, а не unittest.TestCase
. API этих классов одинаковый, но класс Django TestCase настраивает все для тестирования.
Для исполнения тестового набора используйте manage.py
test вместо unittest в командной строке:
$ python manage.py test
Если вам нужно несколько тестовых файлов, замените tests.py на папку tests, положите в нее пустой файл с названием __init__.py
и создайте файлы test_*.py
. Django обнаружит их и выполнит.
Больше информации доступно на сайте документации Django.
Как Пользоваться unittest и Flask
Для работы с Flask приложение необходимо импортировать и перевести в тестовый режим. Вы можете создать тестовый клиент и использовать его для отправки запросов к любым маршрутам в вашем приложении.
Инстанцирование тестового клиента происходит в методе setUp вашего тест-кейса. В следующем примере, my_app — название приложения. Не волнуйтесь, если не знаете, что делает setUp. Познакомимся с этим поближе в разделе «Более Продвинутые Сценарии Тестирования».
Код в тестовом файле будет выглядеть следующим образом:
import my_app
import unittest
class MyTestCase(unittest.TestCase):
def setUp(self):
my_app.app.testing = True
self.app = my_app.app.test_client()
def test_home(self):
result = self.app.get('/')
# Make your assertions
Затем тест-кейсы можно выполнить с помощью команды python -m unittest discover.
Больше информации доступно на сайте документации Flask.
Более Продвинутые Сценарии Тестирования
Перед тем как приступить к созданию тестов для своего приложения, запомните три основных этапа любого теста:
- Создание входных параметров;
- Исполнение кода, получение данных вывода;
- Сравнение данных вывода с ожидаемым результатом;
Это может быть сложнее, чем создание статического значения для исходных данных вроде строки или числа. Иногда ваше приложение требует инстанс класса или контекста. Что же делать в таком случае?
Данные, которые вы создаете в качестве исходных, называют фикстурой. Создание и повторное использование фикстур — распространенная практика.
Запуск одного и того же теста несколько раз с разными значениями в ожидании одного и того же результата называется параметризацией.
Обработка Ожидаемых Сбоев
Ранее, когда мы составляли список сценариев для тестирования sum()
, возник вопрос: что происходит, когда мы предоставляем плохое значение, например, одно целое число или строку?
В таком случае ожидается, что sum()
выдаст ошибку. При появлении ошибки тест провалится.
Есть определенный способ обработки ожидаемых ошибок. Можно использовать .assertRaises()
в качестве контекстного менеджера, а затем внутри блока with
выполнить тестовые шаги:
import unittest
from my_sum import sum
class TestSum(unittest.TestCase):
def test_list_int(self):
"""
Тестируем, что удастся суммировать список целых чисел
"""
data = [1, 2, 3]
result = sum(data)
self.assertEqual(result, 6)
def test_list_fraction(self):
"""
Тестируем, что удастся суммировать список дробных чисел
"""
data = [Fraction(1, 4), Fraction(1, 4), Fraction(2, 5)]
result = sum(data)
self.assertEqual(result, 1)
def test_bad_type(self):
data = "banana"
with self.assertRaises(TypeError):
result = sum(data)
if __name__ == '__main__':
unittest.main()
Этот тест-кейс будет пройден, только если sum(data)
выдаст TypeError. Вы можете заменить TypeError на любой другой тип исключений.
Изоляция Поведений в Приложении
В прошлой части туториала, мы говорили о побочных эффектах. Они усложняют модульное тестирование, так как каждой запуск теста может выдавать разный результат или хуже — один тест может повлиять на состояние всего приложения и вызвать сбой другого теста!
Есть несколько простых методик для тестирования частей приложения с большим количеством побочных эффектов:
- Рефакторинг кода в соответствии с Принципом Единой Ответственности;
- Мокирование всех методов и вызовов функций для устранения побочных эффектов;
- Использование интеграционных тестов вместо модульных для этого фрагмента приложения.
- Если вы не знакомы с мокированием, посмотрите отличные примеры Python CLI Testing.
Написание Интеграционных Тестов
До сих пор мы уделяли больше внимания модульным тестам. Модульное тестирование — отличный способ создания предсказуемого и стабильного кода. Но, в конце концов, ваше приложение должно работать при запуске!
Интеграционное тестирование необходимо для проверки совместной работы нескольких компонентов приложения. Такое тестирование может потребовать отыгрывания роли покупателя или пользователя:
- Вызов HTTP REST API;
- Вызов Python API;
- Вызов веб-сервиса;
- Запуск командной строки.
Все эти виды интеграционных тестов могут быть написаны так же, как и модульные, следуя шаблону Вводные Параметры, Выполнение, Утверждение. Наиболее существенным отличием является то, что интеграционные тесты одновременно проверяют больше компонентов, а значит приведут к большему количеству побочных эффектов, чем модульные тесты. Кроме того, интеграционные тесты требуют наличия большего количества фикстур, например, базы данных, сетевого сокета или файла конфигурации.
Поэтому рекомендуется разделять юнит-тесты и интеграционные тесты. Создание фикстур для интеграционных, например, тестовой базы данных или самих тест-кейсов, занимает гораздо больше времени, чем выполнение юнит-тестов, поэтому стоит проводить интеграционные тесты перед выходом в продакшн вместо их запуска при каждом коммите.
Простейший способ разделить модульные и интеграционные тесты — разнести их по разным папкам.
project/
│
├── my_app/
│ └── __init__.py
│
└── tests/
|
├── unit/
| ├── __init__.py
| └── test_sum.py
|
└── integration/
├── __init__.py
└── test_integration.py
Выполнить определенную группу тестов можно разными способами. Флаг для уточнения директории источника, -s, может быть добавлен к unittest discover с путем, содержащим тесты:
$ python -m unittest discover -s tests/integration
unittest выдаст все результаты в директории tests/integration.
Тестирование Дата-Ориентированных Приложений
Многим интеграционным тестам требуются бэкенд-данные, например, база данных с определенными значениями. Представим, вам нужен тест для проверки правильности работы приложения с более чем 100 клиентами в базе данных, или проверки корректности отображения страницы заказа, даже если все названия товаров на японском.
Такие типы интеграционных тестов будут зависеть от различных тестовых фикстур, чтобы гарантировать их повторяемость и предсказуемость.
Тестовые данные стоит хранить в папке fixtures внутри директории интеграционных тестов, чтобы подчеркнуть их “тестовость”. Затем в тестах можно загрузить данные и запустить тест.
Вот пример структуры данных, состоящих из JSON файлов:
project/
│
├── my_app/
│ └── __init__.py
│
└── tests/
|
└── unit/
| ├── __init__.py
| └── test_sum.py
|
└── integration/
|
├── fixtures/
| ├── test_basic.json
| └── test_complex.json
|
├── __init__.py
└── test_integration.py
В тест-кейсе можно использовать метод .setUp() для загрузки тестовых данных из файла фикстуры по известному пути и выполнить несколько тестов с этими данными. Помните, что можно хранить несколько тест-кейсов в одном файле Python, unittest найдет и выполнит их. Можно иметь по одному тест-кейсу на каждый набор тестовых данных:
import unittest
class TestBasic(unittest.TestCase):
def setUp(self):
# Load test data
self.app = App(database='fixtures/test_basic.json')
def test_customer_count(self):
self.assertEqual(len(self.app.customers), 100)
def test_existence_of_customer(self):
customer = self.app.get_customer(id=10)
self.assertEqual(customer.name, "Org XYZ")
self.assertEqual(customer.address, "10 Red Road, Reading")
class TestComplexData(unittest.TestCase):
def setUp(self):
# load test data
self.app = App(database='fixtures/test_complex.json')
def test_customer_count(self):
self.assertEqual(len(self.app.customers), 10000)
def test_existence_of_customer(self):
customer = self.app.get_customer(id=9999)
self.assertEqual(customer.name, u"バナナ")
self.assertEqual(customer.address, "10 Red Road, Akihabara, Tokyo")
if __name__ == '__main__':
unittest.main()
Если ваше приложение зависит от данных из удаленной локации, например удаленного API, убедитесь, что тесты повторяемы. Разработка может затянуться из-за тестов, провалившихся при отключении API и проблемах со связью. В таких случаях, лучше хранить удаленные фикстуры локально для повторного их вызова и отправки приложению.
В библиотеке requests
есть бесплатный пакет responses, позволяющий создавать ответные фикстуры и сохранять их в тестовых папках. Узнайте больше на их странице GitHub.
В следующей части будет про тестирование в нескольких окружениях и автоматизацию тестирования.
THE END
Комментариивопросы, как всегда приветствуются. Тут или заходите к Стасу на день открытых дверей.
Автор: MaxRokatansky