Тестирование приложений в Django

Тестирование приложений в Django

Сегодня вы узнаете, как проверить работоспособность веб приложения использующего Python фреймворк Django, написав автоматические тесты, которые проверяют страницы и логику кода.

Введение

Я предполагаю, что если вы читаете эту статью, то у вас либо есть проект на Django, либо вы рассматриваете возможность работы с Django для создания нового проекта. Если это правда, подумайте о своем проекте и о том, как бы вы могли проверить его функционал, и убедиться что он работает.

Когда вы начинаете проект, будь то по учебнику или что-то реальное, что вы планируете развивать, помните, что молодой сайт имеет очень мало функциональности. Чтобы проверить, работает ли сайт, вы можете запустить локальный веб-сервер, открыть браузер, перейти к URL-адресу localhost и подтвердить работоспособность сайта. Сколько времени это займет? 5 секунд? 15 секунд? 30 секунд?

Для начала достаточно вручную проверить свой сайт. Однако что происходит, когда вы создаете большее количество страниц? Как вы будете проверять, что все ваши страницы функционируют? Вы можете открыть локальный сайт и начать щелкать по нему, но время, потраченное на подтверждение того, что все работает, начинает расти. Возможно, ваши усилия при проверке занимают 3 минуты, 5 минут или, возможно, гораздо больше. Если вы не будете осторожны, ваше творение может начать ощущаться как мифическая многоголовая Гидра, и то, что когда-то было забавным проектом, превращается в рутину утомительной проверки страниц.

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

Именно здесь на первый план выходят автоматизированные тесты. Автоматизированные тесты позволяют компьютерам делать то, что они делают лучше всего: выполнять повторяющиеся задачи многократно, последовательно и быстро. Когда мы пишем тесты, наша цель — подтвердить некоторую логику или поведение детерминированным способом.

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

def test_sum_function():
    assert sum(40, 2) == 42

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

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

Увидев глупый пример функции суммирования, вы не очень поможете себе с тем, как вы должны тестировать свой проект Django. Далее я покажу некоторые типы тестов для Django. Если вы добавите такие тесты в свой проект, вы сможете вносить изменения на свой сайт с большей уверенностью, что вы ничего не сломаете.

Типы тестов в Django

Я заметил, что всегда удаляю tests.py файл, который поставляется вместе с командой startapp. Причина, по которой я это делаю, заключается в том, что существуют различные виды тестов, и я хочу, чтобы эти различные виды жили в отдельных файлах. Мои приложения имеют эти отдельные файлы в пакете тестов внутри приложения вместо модуля tests.py.

Мой пакет тестов часто отражает структуру самого приложения. Программа, выполняющая тесты, которая называется “test runner”, обычно ожидает найти тесты в файлах, которые начинаются с test_. Пакет часто включает в себя:

  • test_forms.py
  • test_models.py
  • test_views.py
  • И другие.

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

Ответ на этот вопрос влияет на поведение тестов. Если мы пишем тест, который запускает много кода, то мы выигрываем, проверяя сразу много систем; однако есть некоторые недостатки:

  • Запуск большого количества кода означает, что может произойти больше вещей, и существует более высокая вероятность того, что ваш тест сломается неожиданным образом. Тест, который часто ломается неожиданным образом, называется “хрупким” тестом.
  • Запуск большого количества кода означает, что есть много кода для запуска. Это аксиома, но подразумевается, что тест с большим количеством кода для выполнения займет больше времени. Большие автоматизированные тесты по-прежнему, будут намного быстрее, чем тот же тест, выполняемый вручную, поэтому время выполнения относительно.

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

На другом конце находятся тесты, которые выполняют очень мало кода. Хорошим примером является тест функции sum. Эти виды тестов проверяют отдельные единицы кода (например, модель Django). По этой причине мы называем их модульными тестами. Модульные тесты хороши для проверки фрагмента кода в изоляции, чтобы подтвердить его поведение.

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

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

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

Мы должны рассмотреть еще один аспект этого обсуждения: каков “правильный” объем кода для модульного теста? Здесь нет абсолютно правильного ответа. На самом деле эта тема горячо обсуждается среди тестировщиков.

Некоторые люди будут утверждать, что модульный тест должен запускать только код для этого блока. Если у вас есть класс, который реализует некоторую чистую логику и не нуждается в другом коде, то вы находитесь в идеальном случае. Но что произойдет, если вы тестируете метод, добавленный в модель Django, которая должна взаимодействовать с базой данных? Даже если единственное, что вы тестируете, — это метод индивидуальной модели, пурист модульного теста подчеркнет, что тест на самом деле является интеграционным тестом, если он взаимодействует с базой данных.

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

Вот мои рабочие определения модульных и интеграционных тестов в Django. Эти определения несовершенны (как и любые другие определения), но они должны помочь сформулировать обсуждение в этой статье:

  • Модульные тесты — это тесты, которые проверяют отдельные блоки в рамках проекта Django, такие, как метод модели или форма.
  • Интеграционный тест — это тест, который проверяет группу блоков и их взаимодействие, например, проверяет, отображает ли представление ожидаемый результат.

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

Модульные тесты

Когда мы перейдем к некоторым примерам, мне нужно представить несколько инструментов, которые я использую во всех своих проектах Django. Я опишу эти инструменты более подробно в следующем разделе, но они нуждаются в кратком введении здесь, иначе мои примеры не будут иметь большого смысла. Мои два пакета “must have” — это:

  • pytest-django
  • factory-boy

pytest-django — это пакет, который позволяет запускать тесты Django через программу pytest. pytest — это чрезвычайно популярный инструмент тестирования Python с огромной экосистемой расширений. На самом деле pytest-django — это одно из таких расширений.

Моя самая большая причина использования pytest-django заключается в том, что он позволяет мне использовать ключевое слово assert во всех моих тестах. В модуле unittest стандартной библиотеки Python и, как следствие, во встроенных тестовых инструментах Django, которые являются подклассами классов unitttest, проверка значений требует таких методов, как assertEqual и assertTrue. Как мы увидим, использование исключительно ключевого слова assert — очень естественный способ написания тестов.

Другой жизненно важный инструмент в моем поясе инструментов — factory-boy. factory_boy — это инструмент для построения тестовых данных базы данных. Библиотека имеет фантастическую интеграцию с Django и дает нам возможность легко генерировать модельные данные.

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

Тестирование моделей

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

Я собираюсь дать вам основу для любого из ваших тестов, а не только для модульных тестов. Эта структура должна помочь вам рассуждать через любые тесты, с которыми вы сталкиваетесь при чтении и написании кода. Фреймворк — это AAA pattern. AAA pattern расшифровывается как:

  • Arrange — это часть теста, которая устанавливает ваши данные и любые необходимые предварительные условия для вашего теста.
  • Act — этот этап заключается в том, что ваш тест запускает код приложения, который вы хотите протестировать.
  • Assert — последняя часть проверяет, что ваше действие является тем, что вы ожидали.

Для модельного теста это выглядит следующим образом:

Тестирование приложений в Django

Мы можем представить себе проект, который включает в себя систему электронной коммерции. Большая часть обработки заказов — это отслеживание статуса. Мы могли бы вручную установить поле статуса во всем приложении, но изменение статуса внутри метода дает нам возможность делать другие вещи. Например, возможно, метод ship также запускает отправку электронной почты.

В приведенном выше тесте мы проверяем переход состояния от PENDING (ОЖИДАЮЩЕГО) к SHIPPED (ОТГРУЖЕННОМУ). Тест действует на метод ship, а затем обновляет экземпляр модели из базы данных, чтобы убедиться, что состояние SHIPPED сохраняется.

Каковы хорошие стороны этого теста?

Тест включает в себя строку документации. Поверьте мне, вы выиграете от docstrings (эту тему я раскрыл ранее в другой статье) на ваших тестах. Существует сильное искушение оставить все в test_shipped, но в будущем вам может не хватить контекста.

Многие разработчики предпочитают вместо этого длинные имена тестов. Хотя у меня нет проблем с длинными описательными именами тестов, docstrings тоже полезны. Пробелы — это хорошо, и, на мой взгляд, легче прочитать “Виджет обновляет состояние игры при нажатии”, чем test_widget_updates_game_state_when_pushed.

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

И наоборот, вы, скорее всего, столкнетесь с тестами в проектах, которые делают много начальной компоновки, а затем чередуют строки act и assert в одном тесте. Эти виды тестов хрупки (то есть тест может легко сломаться и потерпеть неудачу) и трудно понять, когда происходит сбой.

Качества в этом тесте переводятся на множество различных типов тестов. Я думаю, что в этом вся прелесть наличия надежной ментальной модели для тестирования. Как только вы увидите, как проходит тест:

  • Настройте входы.
  • Примите меры.
  • Проверьте выходы.

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

Тестирование форм

При написании тестов мы часто хотим написать тест “счастливый путь”. Такой тест — это когда все работает именно так, как вы надеетесь. Это тест формы счастливого пути:

Тестирование приложений в Django

С помощью этого теста мы синтезируем POST-запрос. Вот что делает тест:

  • Создает словарь с данными
  • Создает связанную форму (соединяет данные в конструкторе).
  • Проверяет правильность формы.
  • Сохраняет форму.
  • Утверждает, что была создана новая запись.

Обратите внимание, что я немного отклоняюсь от правил ААА для этого теста. Часть соглашения Django для форм заключается в том, что форма действительна до вызова метода save. Если это соглашение не соблюдается, то cleaned_data не будет заполняться правильно, и большинство методов сохранения зависят от cleaned_data. Несмотря на то, что is_valid — это действие, я рассматриваю его как шаг настройки для тестов формы.

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

Тестирование приложений в Django

Тест показывает механику проверки недействительной формы. Ключевыми элементами являются:

  • Настройка данных плохой формы.
  • Проверка валидности с помощью is_valid.
  • Проверка состояния вывода в форме.

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

Как правило, я не думаю, что полезно тестировать функции из самого фреймворка. Хороший проект с открытым исходным кодом, такой как Django, уже тестирует эти функции для вас. Когда вы пишете тесты формы, вы должны проверить пользовательские методы clean_* и clean, а также любой пользовательский метод сохранения, который вы можете добавить.

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

Интеграционные тесты

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

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

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

  • Родное мобильное приложение.
  • Интерфейс командной строки.
  • Библиотека, такая как requests, которая может обрабатывать HTTP-запросы и ответы.

Тестовый клиент Django похож на другие клиенты тем, что он может взаимодействовать с вашим проектом Django для получения данных из создаваемых им запросов. Хорошая часть тестового клиента заключается в том, что выходные данные возвращаются удобным способом, против которого мы можем производить утверждения. Клиент возвращает объект HttpResponse напрямую!

Вот интеграционный тест, который мы можем обсудить:

Тестирование приложений в Django

Что делает этот тест? А что этот тест не делает?

Используя тестовый клиент Django, тест запускает много кода Django:

  • Маршрутизацию URL-адресов.
  • Просмотр представлений (которые, скорее всего, будут извлечены из базы данных).
  • Рендеринг шаблона.

Это много кода для выполнения в одном тесте! Цель теста — проверить, что все основные части совпадают.

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

Когда я пишу интеграционный тест, я в основном пытаюсь ответить на вопрос: держится ли система вместе, не ломаясь?

Теперь, когда мы рассмотрели модульные тесты и интеграционные тесты, какие инструменты помогут вам упростить тестирование?

Полезные инструменты

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

Я собираюсь вернуться к инструментам, которые я перечислил ранее, pytest-django и factory_boy. Считайте, что это ваш набор выживания для тестирования Django. По мере развития навыков тестирования вы можете добавлять новые инструменты в свой набор инструментов, но эти два инструмента — фантастическое начало.

Библиотека pytest-django

pytest — это тестовый раннер. Задача инструмента — выполнять автоматизированные тесты. Если вы прочитаете описание и запуск тестов в документации Django, вы обнаружите, что Django также включает в себя тестовый раннер ./manage.py test. Что это дает? Почему я предлагаю вам использовать pytest?

Я собираюсь сделать смелое утверждение: pytest — лучший. Мне очень нравится встроенный тестовый раннер Django, но я постоянно возвращаюсь к pytest по одной основной причине: я могу использовать assert в тестах. Как вы видели в этих тестовых примерах, ключевое слово assert обеспечивает четкое чтение. Мы можем использовать все обычные сравнительные тесты Python (например, ==, !=, in) для проверки выходных данных тестов.

Тестовый раннер Django строится на основе тестовых инструментов, которые входят в состав Python в модуле unittest. С помощью этих инструментов тестирования разработчики должны создавать тестовые классы, которые являются подклассами unittest. Есть такое понятие, как TestCase. Недостатком классов TestCase является то, что для проверки кода необходимо использовать набор методов assert*.

Список методов assert* включен в документацию unittest. Вы можете быть очень успешны с этими методами, но я думаю, что это требует запоминания API, который включает в себя большое количество методов. Подумайте вот о чем. Что бы вы предпочли?

  • Использовать assert
  • Использовать assertEqual, assertNotEqual, assertTrue, assertFalse, assertIs, assertIsNot, assertIsNone, assertIsNotNone, assertIn, assertNotIn, assertIsInstance и assertNotIsInstance

Использование assert от pytest означает, что вы получаете все преимущества методов assert*, но вам нужно запомнить только одно ключевое слово. Если этого было недостаточно, давайте сравним читабельность:

Тестирование приложений в Django

По той же причине, по которой разработчики Python предпочитают методы свойств вместо геттеров и сеттеров (например, obj.value = 42 вместо obj.set_value(42)), я думаю, что синтаксис стиля assert гораздо проще визуально обрабатывать.

Помимо потрясающей обработки assert, pytest-django включает в себя множество других функций, которые могут показаться вам интересными при написании автоматических тестов.

Библиотека factory_boy

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

factory_boy поможет вам построить модель данных для ваших тестов.

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

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

Самая большая проблема с использованием create связана с ограничениями базы данных, такими как внешние ключи. Что вы делаете, если хотите создать запись, которая требует большого количества ненулевых отношений внешнего ключа? Ваш единственный выбор — создать эти записи внешнего ключа.

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

Тестирование приложений в Django

На первый взгляд, тест не так уж плох. Я думаю, что это в основном потому, что я держал моделирование простым. Что, если DirectorProducer, или Studio  также требовали внешних ключей? Мы потратили бы большую часть наших усилий на раздел аранжировки теста. Кроме того, когда мы осматриваем тест, мы увязаем в ненужных деталях. Нужно ли было знать имена DirectorProducer, или Studio? Нет, для этого теста нам это было не нужно. Теперь давайте посмотрим на эквивалент factory_boy:

Тестирование приложений в Django

MovieFactory кажется волшебной. Наш тест должен был игнорировать все остальные детали. Теперь тест мог полностью сосредоточиться на жанре.

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

Как может выглядеть эта MovieFactory? Фабрика может быть:

Тестирование приложений в Django

Это фабричное определение очень декларативно. Мы объявляем, чего хотим, и factory_boy придумывает, как это сделать. Это качество приводит к фабрикам, о которых вы можете рассуждать, потому что вы можете сосредоточиться на том, что есть, а не на том, как построить модель.

Другой примечательный аспект заключается в том, что фабрики составляют вместе. Когда мы вызываем MovieFactory(), factory_boy пропускает данные обо всем, поэтому он должен собрать все эти данные. Проблема в том, что MovieFactory не знает, как построить Producer или какие-либо внешние ключевые отношения фильма. Вместо этого фабрика будет делегировать полномочия другим фабрикам, используя атрибут SubFactory. Делегируя полномочия другим фабрикам, factory_boy может построить модель и все ее дерево отношений с помощью одного вызова.

Когда мы хотим переопределить поведение некоторых сгенерированных данных, мы передаем дополнительный аргумент, как я сделал во втором примере, предоставляя “Sci-Fi” в качестве жанра. Вы можете передать и другие экземпляры модели на свои заводы.

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

Заключение

В этой статье мы рассмотрели тесты с проектами Django. Мы сосредоточились на:

  • Зачем кому-то писать автоматические тесты.
  • Какие виды тестов полезны для приложения Django.
  • Какие инструменты вы можете использовать для облегчения тестирования.

На этом все. Следите за моим блогом и узнавайте больше полезной информации!

Егор Егоров

Программирую на Python с 2017 года. Люблю создавать контент, который помогает людям понять сложные вещи. Не представляю жизнь без непрерывного цикла обучения, спорта и чувства юмора.

Ссылка на мой github есть в шапке. Залетай.

Оцените автора
Егоров Егор
Добавить комментарий

  1. Анастасия

    Как получить

    Ответить
  2. Антон

    Где узнать про

    Ответить
  3. Александр

    Нужен

    Ответить
  4. Александр

    Спасибо, отличная статья, для начала самое то !!!

    Ответить