Всем привет!

Продолжаем статью о знакомстве с тестированием в 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.

Более Продвинутые Сценарии Тестирования

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

  1. Создание входных параметров;
  2. Исполнение кода, получение данных вывода;
  3. Сравнение данных вывода с ожидаемым результатом;

Это может быть сложнее, чем создание статического значения для исходных данных вроде строки или числа. Иногда ваше приложение требует инстанс класса или контекста. Что же делать в таком случае?

Данные, которые вы создаете в качестве исходных, называют фикстурой. Создание и повторное использование фикстур — распространенная практика.

Запуск одного и того же теста несколько раз с разными значениями в ожидании одного и того же результата называется параметризацией.

Обработка Ожидаемых Сбоев

Ранее, когда мы составляли список сценариев для тестирования 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/
¦ L-- __init__.py
¦
L-- tests/
|
+-- unit/
| +-- __init__.py
| L-- test_sum.py
|
L-- integration/
+-- __init__.py
L-- test_integration.py


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


$ python -m unittest discover -s tests/integration

unittest выдаст все результаты в директории tests/integration.

Тестирование Дата-Ориентированных Приложений

Многим интеграционным тестам требуются бэкенд-данные, например, база данных с определенными значениями. Представим, вам нужен тест для проверки правильности работы приложения с более чем 100 клиентами в базе данных, или проверки корректности отображения страницы заказа, даже если все названия товаров на японском.

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

Тестовые данные стоит хранить в папке fixtures внутри директории интеграционных тестов, чтобы подчеркнуть их “тестовость”. Затем в тестах можно загрузить данные и запустить тест.

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

project/
¦
+-- my_app/
¦ L-- __init__.py
¦
L-- tests/
|
L-- unit/
| +-- __init__.py
| L-- test_sum.py
|
L-- integration/
|
+-- fixtures/
| +-- test_basic.json
| L-- test_complex.json
|
+-- __init__.py
L-- 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

Комментарии\вопросы, как всегда приветствуются. Тут или заходите к Стасу на день открытых дверей.

Комментарии (1)


  1. vovochkin
    19.12.2018 08:48

    Вот как выглядит разделение интеграционных и модульных тестов в вашей статье:

    Неправильная картинка