Перевод статьи Anthony Shaw: Getting Started With Testing in Python Статья будет интересна тем кто еще мало знаком с тестированием в Python и быстро получить обзорные знания для дальнейшего изучения.
Это руководство предназначено для тех, кто уже имеет опыт написания приложений в Python, но еще не написал ни одного теста.
Тестирование в Python — это огромная тема, которая может быть очень сложной, хотя и не обязательно должно быть сложно. Вы можете приступить к созданию простых тестов для своего приложения за несколько простых шагов.
В этом руководстве вы узнаете, как создать простой тест, как его выполнить и находить ошибки раньше, чем ваши пользователи! Вы узнаете об инструментах, доступных для написания и выполнения тестов, померить производительность своего приложения и даже как найти проблемы с безопасностью.
Есть много способов проверить ваш код. В этом учебном пособии вы изучите приемы состоящие из самых простых шагов и далее перейдете к более продвинутым методам.
Хорошая новость в том, что вы, вероятно, уже создавали тесты, не осознавая этого. Помните, когда вы запускали приложение и использовали его впервые? Вы проверяли функции и экспериментировали с ними? Это называется исследовательское тестирование и является формой ручного тестирования.
Исследовательское тестирование — это форма тестирования, которая проводится без плана. В таком виде тестирования вы просто изучаете приложение.
Чтобы получить полный набор ручных тестов, все, что вам нужно сделать, это составить список всех функций, которыми обладает ваше приложение, список различных типов входных данных, которые оно может принять, и все ожидаемые результаты. Теперь, каждый раз, когда вы будете вносите изменения в свой код, вам нужно просмотреть каждый элемент в этом списке и проверить его правильность.
Это не похоже на веселье, не так ли?
Вот где приходит на помощь автоматическое тестирование. Автоматизированное тестирование — это выполнение плана тестирования (частей вашего приложения, которые вы хотите протестировать, а так же порядок, в котором вы хотите их тестировать, и ожидаемые результаты) с помощью сценария тестирования. В Python есть инструменты и библиотеки, которые помогут вам создавать автоматизированные тесты для вашего приложения. Далее мы рассмотрим эти инструменты и библиотеки.
В мире тестирования нет недостатка в терминологии, и теперь, когда вы знаете разницу между автоматическим и ручным тестированием, пришло время перейти на уровень глубже.
Подумайте, как вы можете проверить свет в автомобиле. Вы должны включить свет (это будет называться один шаг теста (test step)), далее нужно выйти из машины или попросить друга проверить, включены ли огни (это называется утверждение теста (test assertion)). Тестирование нескольких компонентов называется интеграционным тестированием (integration testing). Основная проблема с интеграционным тестированием — это когда интеграционный тест не дает правильного результата. Иногда очень трудно диагностировать проблему, не имея возможности определить, какая часть системы вышла из строя. Если свет не включился, то, возможно сломаны лампы или разряжен аккумулятор. А как насчет генератора? Или может быть сломан компьютер машины?
Если у вас модный современный автомобиль, он сообщит вам, когда ваши лампочки вышли из строя. Это делается с помощью модульных тестов (unit test).
Модульный тест — это как правило небольшой тест, который проверяет правильность работы отдельного компонента. Модульный тест поможет вам выделить то, что сломано в вашем приложении, и быстро это исправить.
Вы только что рассмотрели два типа тестов:
В Python вы можете написать как интеграционные, так и модульные тесты. Чтобы написать модульный тест для встроенной функции sum(), вы должны сравнить выходные данные sum() с ожидаемыми выходными данными.
Например, чтобы проверить, что сумма чисел 1, 2, 3 равна 6, можно написать это:
>>> assert sum([1, 2, 3]) == 6, "Should be 6"
В данном случае этот код ничего не будет выводить в REPL (интерактивная оболочка Python), потому что значения верные.
Если результат sum() будет неверен, это приведет к ошибке AssertionError и сообщению «Should be 6». Попробуйте ввести неверные значения, чтобы увидеть ошибку 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, на нужно поместить этот код в новый файл с именем test_sum.py и выполнить его снова:
def test_sum(): assert sum([1, 2, 3]) == 6, "Should be 6" if __name__ == "__main__": test_sum() print("Everything passed")
Таким образом вы создали свой тест (test case), с утверждение (assertion) и точкой входа с командной строки. Сейчас вы можете выполнить его в командной строке:
$ python test_sum.py Everything passed
Вы можете увидеть успешный результат выполнения теста, Everything passed
.
В Python sum() принимает любую итерационный список в качестве первого аргумента. Мы проверили это фукнцию со списком. Теперь давай те протестируем ее с кортежем (tuple). Создайте новый файл с именем 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() c (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 runner). Test runner — это специальное приложение, предназначенное для запуска тестов, проверки выходных данных и предоставления инструментов для отладки и диагностики тестов и приложений.
В Python доступно множество тестовых раннеров. Встроенный в стандартную библиотеку Python, называется unittest. В этом руководстве мы будем использовать тестовые примеры на unittest. Принципы unittest легко переносимы на другие тестовые фреймворки. Три самых популярных test runner:
unittest был встроен в стандартную библиотеку Python начиная с версии 2.1. Вы, вероятно, могли видеть его использование в коммерческих приложениях Python и проектах с открытым исходным кодом.
unittest содержит как тестовую среду, так и test runner. У unittest есть некоторые важные требования для написания и выполнения тестов.
unittest требует, чтобы:
Чтобы преобразовать предыдущий пример в тестовый блок unittest, вам необходимо:
Для этого создадим новый файл 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)
Примечание: будьте осторожны, если вы пишете тесты, которые должны выполняться как в Python 2, так и в 3. В Python 2.7 и ниже unittest называется unittest2. Если вы просто импортируете из unittest, вы получите разные версии с разными функциями между Python 2 и 3.
Для получения дополнительной информации о unittest, вы можете изучить документацию unittest.
Вы можете обнаружить, что когда вы напишете сотни или даже тысячи тестов для своего приложения, будет становится все труднее понять и использовать результаты unittest.
nose совместим с любыми тестами, написанными с использованием среды unittest, и может использоваться в качестве его замены. Развитие nose как приложения с открытым исходным кодом отстало, и был создан форк под названием nose2. Если вы начинаете изучать тестирование с нуля, рекомендуется использовать nose2 вместо nose.
Чтобы начать работу с nose2, установите его из PyPI. nose2 попытается обнаружить все тестовые сценарии с именем test_*.py, унаследованные от unittest.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 предлагает множество флагов командной строки для фильтрации выполняемых вами тестов. Для получения дополнительной информации вы можете изучить документацию по Nose 2.
pytest так же поддерживает созданные тесты с unittest. Настоящее преимущество pytest заключается в написании test case. Test case в pytest представляют собой серию функций в файле Python, начинающихся с имени test_.
У pytest есть и другие замечательные возможности:
Написание теста 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. Внутри my_sum создайте пустой файл с именем __init__.py. Создание файла __init__.py означает, что папку my_sum можно импортировать как модуль из родительского каталога.
Ваша папка проекта должна выглядеть так:
project/ │ └── my_sum/ └── __init__.py
Откройте my_sum/__init__.py и создайте новую функцию с именем sum(), которая будет принимать любой итерацию (список, кортеж или набор set) и возвращать сумму всех сложенных значений вместе:
def sum(arg): total = 0 for val in arg: total += val return total
Чтобы начать писать тесты, вы можете просто создать файл с именем test.py, который будет содержать ваш первый тестовый пример. Поскольку файл должен иметь возможность импортировать ваше приложение, чтобы иметь возможность его протестировать, разместите файл test.py над папкой пакета. Ваше дерево каталогов должно выглядеть как то так:
project/ │ ├── my_sum/ │ └── __init__.py | └── test.py
По мере добавления все большего количества тестов в отдельный файл, очень скоро вы обнаружите, что этот файл становиться все более загроможденным и сложенным в обслуживании. Поэтому лучше создать папку с именем tests/ и разбить тесты на несколько файлов. Принято считать, что каждый файл должен начинается с test_. Некоторые очень большие проекты разбивают каталог tests на несколько подкаталогов в зависимости от их назначения или использования.
Примечание. Что если ваше приложение представляет собой один скрипт? Вы можете импортировать любые атрибуты скрипта, такие как классы, функции и переменные, используя встроенную функцию __import__(). Вместо from my_sum import sum вы можете написать следующее:
target = __import__("my_sum.py") sum = target.sum
Преимущество использования __import__() состоит в том, что вам не нужно превращать папку вашего проекта в пакет, и вы можете просто указать имя файла. Это также полезно, если имя файла конфликтует с какими-либо стандартными пакетами библиотеки. Например, math.py будет конфликтовать с модулем math.
Прежде чем погрузиться в написание тестов, нужно сначала принять пару решений:
Далее структура теста должна следовать этому рабочему процессу:
В нашем приложение sum() вы можете проверить множество вариантов поведения, таких как:
Самый простой тест — это список целых чисел. Создайте файл test.py со следующим кодом Python:
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()
Этот пример кода:
Если вы не уверены, что такое self или как определяется .assertEqual(), вы можете освежить свое объектно-ориентированное программирование с помощью объектно-ориентированного программирования Python 3.
Последний шаг написания теста — проверка вывода по известному ответу. Это известно как утверждение (assertion). Существует несколько общих рекомендаций по написанию утверждений:
unittest поставляется с множеством методов для проверки утверждений значений, типов и существования переменных. Вот некоторые из наиболее часто используемых методов:
Method | Equivalent to |
---|---|
.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() и .assertIsInstance() имеют противоположные методы с именем .assertIsNot() и т. д.
Когда вы пишете тесты, зачастую это не просто проверить возвращаемое значение функции. Часто выполнение фрагмента кода изменяет другие вещи в программе, такие как атрибут класса, файл в файловой системе или значение в базе данных. Это называется побочные эффекты (side effects) и являются важной частью тестирования. Решите, проверяется ли побочный эффект, прежде чем включать его в свой список утверждений.
Если вы обнаружите, что блок кода, который вы хотите протестировать, имеет много побочных эффектов, возможно, вы нарушаете принцип единой ответственности. Нарушение принципа единой ответственности означает, что фрагмент кода выполняет слишком много задач, и было бы лучше, если бы он подвергся рефакторингу. Следование принципу единой ответственности — это отличный способ разработки кода, позволяющего легко создавать повторяемые и простые модульные тесты и, в конечном счете, надежные приложения.
Теперь, когда вы создали первый тест, вы хотите его выполнить. Конечно, вы знаете, что он будет успешен, но прежде чем создавать более сложные тесты, вы должны убедиться, что вы можете успешно выполнить тесты.
Приложение Python, которое выполняет ваш тестовый код, проверяет утверждения и выдает результаты теста в вашей консоли, называется test runner.
В нижней части test.py вы добавили небольшой фрагмент кода:
if __name__ == '__main__': unittest.main()
Это точка входа в тест. Это означает, что если вы выполните скрипт, запустив python test.py в командной строке, он вызовет unittest.main(). Эта команда запускает тестовый раннер, обнаруживая в этом файле все классы, которые наследуются от unittest.TestCase.
Это один из многих способов выполнить тестовый модуль unittest. Если у вас есть один тестовый файл с именем test.py, вызов python test.py — отличный способ начать тестирование.
Другой способ — использовать командную строку unittest. Попробуй эту команду:
$ python -m unittest test
Она выполнит тот же тестовый модуль через командную строку.
Вы можете предоставить дополнительные параметры для изменения вывода. Одним из них является -v для вывода максимального количества сообщений. Попробуйте следующее:
$ python -m unittest -v test test_list_int (test.TestSum) ... ok ---------------------------------------------------------------------- Ran 1 tests in 0.000s
Эта команда выполнила один тест из test.py и распечатала результаты в консоли. В режиме -v (Verbose) перечисляются имена тестов, которые выполняются, а также их результаты.
Вместо предоставления имени модуля, содержащего тесты, можно запустить автообнаружение с помощью следующей команды:
$ 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 перейдет в каталог src/, просканирует все тестовые файлы *.py внутри каталога tests и выполнит их.
Это был очень простой пример, когда все проходит успешно. Теперь попробуем провалить тест и интерпретировать результат.
sum() должна иметь возможность принимать другие списки числовых типов, например дроби.
Вверху файла test.py добавьте оператор import для импорта типа Fraction из модуля fractions:
from fractions import Fraction
Теперь добавьте в тест утверждение, ожидающее неправильное значение, в этом случае ожидая, что сумма 1/4, 1/4 и 2/5 будет 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)
В выходных данных мы видим следующую информацию:
Помните, что вы можете вывести дополнительную информацию в результатах теста, добавив флаг -v к команде python -m unittest.
Если вы используете PyCharm IDE, вы можете запустить unittest или pytest, выполнив следующие действия:
Эта команда выполнит unittest в тестовом окне и выдаст вам результаты в PyCharm:
Более подробная информация доступна на веб-сайте PyCharm.
Если вы используете Microsoft Visual Studio Code IDE, то вы можете использовать поддержку плагинов unittest, nose и pytest которая встроена в плагин Python (требует установки). Для этого вы можете настроить конфигурацию ваших тестов, открыв командную строку с помощью Ctrl + Shift + P и набрав «Python test». Вы увидите ряд вариантов:
Выберите «Отладить все модульные тесты» (Debug All Unit Tests), после чего VSCode отобразит приглашение для настройки инфраструктуры тестирования. Нажмите на винтик (cog), чтобы выбрать тестового раннера (unittest) и домашний каталог тестов ( . ).
После настройки вы увидите статус ваших тестов в нижней части окна, и вы можете быстро получить доступ к журналам тестов и снова запустить тесты, нажав на следующие значки:
Это сообщение показывает, что тесты выполняются, но некоторые из них не проходят.
Если вы пишете тесты для веб-приложения, используя одну из популярных платформ, таких как Django или Flask, существуют некоторые важные различия в том, как вы пишете и запускаете тесты.
Подумайте обо всем коде, который вы собираетесь тестировать в веб-приложении. Все маршруты, вьюхи и модели требуют большого количество импортируемых модулей и знаний об используемых платформах.
Это похоже на автомобильный тест в начале урока: вам нужно запустить компьютер автомобиля, прежде чем вы сможете выполнить простой тест, такой как проверка фар.
Django и Flask упрощают эту задачу, предоставляя среду тестирования на основе unittest.
Шаблон Django startapp создаст файл tests.py в каталоге вашего приложения. Если у вас его еще нет, вы можете создать его со следующим содержимым:
from django.test import TestCase class MyTestCase(TestCase): # Your test methods
Основное различие с примерами состоит в том, что вам нужно наследоваться от django.test.TestCase вместо unittest.TestCase. Эти классы имеют одинаковый API, но класс Django TestCase устанавливает необходимое состояние для тестирования.
Чтобы выполнить ваш набор тестов, вместо использования unittest в командной строке, вы используете manage.py:
$ python manage.py test
Если вам нужно несколько тестовых файлов, замените tests.py на папку с именем tests, вставьте в нее пустой файл с именем __init__.py и создайте файлы test_*.py. Django обнаружит и выполнит их.
Более подробная информация доступна на веб-сайте документации Django.
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.
Прежде чем приступить к созданию тестов для своего приложения, запомните три основных этапа каждого теста:
Это не всегда так просто, как создать статическое значение для ввода, например, строку или число. Иногда вашему приложению потребуется экземпляр какого нибудь класса или много входные данных. Что вы сделаете тогда?
Данные, которые создаются в качестве входных данных, известны как fixture. Обычной практикой является создание fixture и их повторное использование.
Если вы выполняете один и тот же тест и каждый раз передаете разные значения и ожидаете одного и того же результата, это называется параметризацией.
Ранее, когда вы составляли список сценариев для проверки sum(), возникал вопрос: что произойдет, если вы предоставите ему неверное значение, такое как одно целое число или строка?
В этом случае вы ожидаете, что sum() выдаст ошибку. Когда он выдает ошибку, это может привести к сбою теста.
Существует специальный способ обработки ожидаемых ошибок. Вы можете использовать .assertRaises() в качестве менеджера контекста, затем внутри блока with выполнить тестовые шаги:
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) def test_bad_type(self): data = "banana" with self.assertRaises(TypeError): result = sum(data) if __name__ == '__main__': unittest.main()
Этот тестовый сценарий теперь будет проходить, только если sum(data) вызывает ошибку TypeError. Вы можете заменить TypeError любым типом исключения, который вы выберете.
Ранее в статье мы рассказали , что такое побочный эффект. Побочные эффекты усложняют юнит-тестирование, поскольку при каждом запуске теста он может давать другой результат или, что еще хуже, один тест может повлиять на состояние целого приложения и вызвать сбой другого теста!
Есть несколько простых методов, которые вы можете использовать для тестирования отдельных частей вашего приложения, которые имеют много побочных эффектов:
Если вы не знакомы с mocking, почитайте Python CLI Testing, чтобы найти отличные примеры.
До сих пор мы рассказывали главным образом о модульном тестирование. Модульное тестирование — отличный способ создать предсказуемый и стабильный код.
Интеграционное тестирование — это тестирование нескольких компонентов приложения для проверки их совместной работы. Интеграционное тестирование может потребовать от пользователя или пользователя приложения:
Каждый из этих типов интеграционных тестов может быть написан так же, как модульный тест, следуя шаблонам Input, Execute и Assert. Наиболее существенным отличием является то, что интеграционные тесты проверяют одновременно большое количество компонентов и, следовательно, будут иметь больше побочных эффектов, чем модульное тестирование. Кроме того, интеграционные тесты потребуют наличия большего количества дополнений, таких как база данных, сетевой сокет или файл конфигурации.
Вот почему рекомендуется разделять юнит-тесты и интеграционные тесты. Создание дополнений, необходимых для интеграции, таких как тестовая база данных, и сами тесты часто выполняются намного дольше, чем модульные тесты, поэтому рекомендуется запускать интеграционные тесты только перед тем, как приступить к работе, а не при каждом коммите.
Простой способ разделения юнит-тестов и интеграционных тестов — просто поместить их в разные папки:
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() для загрузки тестовых данных из файла fixure по известному пути.
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 не работает или существует проблема с подключением, которая может замедлить разработку. В таких ситуациях рекомендуется хранить удаленные fixture локально, чтобы их можно было вызывать и отправлять в приложение.
Библиотека requests имеет бесплатный пакет, называемый responses, который дает вам возможность создавать fixture ответов и сохранять их в своих тестовых папках. Узнайте больше об этом на их странице GitHub.
До сих пор мы рассказывали о тестирование в одной версии Python, используя виртуальную среду с определенным набором зависимостей. Возможно, вы захотите проверить, работает ли ваше приложение в нескольких версиях Python или на нескольких версиях пакета. Tox — это приложение, которое автоматизирует тестирование одновременно в нескольких средах.
Tox доступен в PyPI в виде пакета для установки через pip:
$ pip install tox
Теперь, когда у вас установлен Tox, его необходимо настроить.
Tox настраивается через файл конфигурации в каталоге вашего проекта. Файл конфигурации Tox содержит следующее:
Вместо того, чтобы изучать синтаксис конфигурации Tox, вы можете сделать быстрый старт, запустив приложение быстрого запуска:
$ tox-quickstart
Инструмент настройки Tox задаст вам эти вопросы и создаст файл, подобный следующему в tox.ini:
[tox] envlist = py27, py36 [testenv] deps = commands = python -m unittest discover
Прежде чем вы сможете запустить Tox, он должен иметь файл setup.py в папке вашего приложения, содержащий шаги для установки вашего пакета. Если у вас его нет, вы можете следовать этому руководству о том, как создать файл setup.py, прежде чем продолжить.
Кроме того, если ваш проект не предназначен для распространения через PyPI, вы можете пропустить это требование, добавив следующую строку в файл tox.ini под заголовком [tox]:
[tox] envlist = py27, py36 skipsdist=True
Если вы не создадите файл setup.py, а ваше приложение имеет некоторые зависимости от PyPI, вам необходимо указать их в нескольких строках в разделе [testenv]. Например, Django потребует следующее:
[testenv] deps = django
После того, как вы завершили этот этап, вы готовы запустить тесты.
Теперь вы можете выполнить Tox, и он создаст две виртуальные среды: одну для Python 2.7 и одну для Python 3.6. Каталог Tox называется .tox/. В каталоге .tox/ Tox выполнит обнаружение модуля python -m unittest для каждой виртуальной среды.
Вы можете запустить этот процесс, вызвав Tox в командной строке:
$ tox
Tox выдаст результаты ваших тестов для каждой среды. При первом запуске Tox требуется немного времени для создания виртуальных сред, но когда это произойдет, второе выполнение будет намного быстрее.
Вывод Tox довольно прост. Он создает среду для каждой версии, устанавливает ваши зависимости, а затем запускает тестовые команды.
Есть несколько дополнительных параметров командной строки, которые могут пригодится.
Запускает только одну среду, такую как Python 3.6:
$ tox -e py36
Создает заново виртуальные среды, если ваши зависимости изменились или пакеты оказались повреждены:
$ tox -r
Запускает Tox с меньшим объемом вывода сообщений:
$ tox -q
Запускает Tox с более подробными сообщениями:
$ tox -v
Более подробную информацию о Tox можно найти на веб-сайте документации Tox.
До сих пор вы выполняли тесты вручную, запуская команды. Существуют некоторые инструменты для автоматического выполнения тестов, когда вы вносите изменения и фиксируете их в репозитории системы управления версиями, такой как Git. Инструменты автоматического тестирования часто называют инструментами CI/CD, что означает «Непрерывная интеграция/Непрерывное развертывание» (Continuous Integration/Continuous Deployment). Они могут запускать ваши тесты, компилировать и публиковать любые приложения и даже развертывать их в рабочей среде.
Travis CI является одним из многих доступных сервисов CI (Continuous Integration).
Travis CI прекрасно работает с Python, и когда вы создадите все свои тесты, вы можете автоматизировать их выполнение в облаке! Travis CI бесплатен для любых проектов с открытым исходным кодом на GitHub и GitLab и доступен для частных проектов.
Для начала войдите на веб-сайт и выполните аутентификацию, используя свои учетные данные GitHub или GitLab. Затем создайте файл с именем .travis.yml со следующим содержимым:
language: python python: - "2.7" - "3.7" install: - pip install -r requirements.txt script: - python -m unittest discover
Эта конфигурация инструктирует Travis CI:
После того как вы закоммите и отправите этот файл, Travis CI будет запускать эти команды каждый раз, когда вы будете пушить в удаленный репозиторий Git.
Теперь, когда вы узнали, как создавать тесты, выполнять их, включать их в свой проект и даже выполнять их автоматически, есть несколько передовых методов, которые могут оказаться полезными по мере роста вашей библиотеки тестов.
Tox и Travis CI имеют конфигурацию для команд запуска тестирования. Команда тестирования, которую вы использовали в этом руководстве, — это python -m unittest discover.
Вы можете использовать одну или несколько команд во всех этих инструментах, и эта опция позволяет вам добавлять больше инструментов, которые улучшают качество вашего приложения.
Один из таких типов приложений называется линтер (linter). Линтер проверяет ваш код и комментирует его. Он может дать вам советы об ошибках, которые вы сделали, исправить пробелы и даже предсказать ошибки, которые вы, возможно, внесли.
Для получения дополнительной информации о линтерах прочитайте руководство по качеству кода Python.
Популярный линтер, который комментирует стиль вашего кода по отношению к спецификации PEP 8, — это flake8.
Вы можете установить flake8 через pip:
$ pip install flake8
Затем вы можете запустить flake8 для одного файла, папки или шаблона:
$ flake8 test.py test.py:6:1: E302 expected 2 blank lines, found 1 test.py:23:1: E305 expected 2 blank lines after class or function definition, found 1 test.py:24:20: W292 no newline at end of file
Вы увидите список ошибок и предупреждений для вашего кода, который обнаружил flake8.
flake8 настраивается в командной строке или в файле конфигурации вашего проекта. Если вы хотите игнорировать определенные правила, такие как E305, показанный выше, вы можете установить их в конфигурации. flake8 проверит файл .flake8 в папке проекта или файл setup.cfg. Если вы решили использовать Tox, вы можете поместить раздел конфигурации flake8 в tox.ini.
В этом примере игнорируются каталоги .git и __pycache__, а также правило E305. Кроме того, он устанавливает максимальную длину строки в 90 вместо 80 символов. Вероятно, вы обнаружите, что ограничение по умолчанию в 79 символов для длины строки очень ограничивает тесты, поскольку они содержат длинные имена методов, строковые литералы с тестовыми значениями и другие фрагменты данных, которые могут быть длиннее. Обычно для тестов устанавливается длина строки до 120 символов:
[flake8] ignore = E305 exclude = .git,__pycache__ max-line-length = 90
Кроме того, вы можете предоставить эти параметры в командной строке:
$ flake8 --ignore E305 --exclude .git,__pycache__ --max-line-length=90
Полный список параметров конфигурации доступен на веб-сайте документации.
Теперь вы можете добавить flake8 к вашей конфигурации CI. Для Travis CI это будет выглядеть следующим образом:
matrix: include: - python: "2.7" script: "flake8"
Travis CI прочтет конфигурацию в .flake8 и завершит процесс сборки, если возникнут какие-либо ошибки со сборкой. Обязательно добавьте зависимость flake8 в ваш файл requirements.txt.
flake8 — пассивный линтер: он рекомендует изменения, но сам ничего не меняет в коде. Более агрессивный подход — это средство форматирования кода. Форматировщики кода автоматически изменят ваш код в соответствии с практикой стилей и макетов.
black очень хороший форматер. У него нет опций конфигурации, и у него очень специфический стиль. Это делает его отличным инструментом для вставки в ваш тестовый конвейер.
Примечание: black требует Python 3.6+.
Вы можете установить black через pip:
$ pip install black
Затем, чтобы запустить black в командной строке, укажите файл или каталог, который вы хотите отформатировать:
$ black test.py
При написании тестов вы можете обнаружить, что в конечном итоге вы копируете и вставляете код намного чаще, чем в обычных приложениях. Время от времени тесты могут быть очень повторяющимися, но это ни в коем случае не является причиной, по которой ваш код должен оставаться неаккуратным и трудным в обслуживании.
Со временем у вас будет большой технический долг в вашем тестовом коде, и если у вас будут значительные изменения в приложении, которые требуют изменений в ваших тестах, это может оказаться более трудоемкой задачей, чем необходимо, из-за того, как они были структурированы.
При написании тестов старайтесь придерживаться принципа DRY: (Don’t Repeat Yourself (не повторяйте себя).
Тестовые fixture и функции являются отличным способом создания тестового кода, который легче поддерживать. Кроме того, удобочитаемость так же имеет значение. Подумайте о развертывании инструмента помощи, например flake8, поверх своего тестового кода:
$ flake8 --max-line-length=120 tests/
Есть много способов для тестирования производительности кода в Python. Стандартная библиотека предоставляет модуль timeit, который может запускаться несколько раз и давать вам оценку производительности. Этот пример выполнит test() 100 раз и отобразит вывод:
def test(): # ... your code if __name__ == '__main__': import timeit print(timeit.timeit("test()", setup="from __main__ import test", number=100))
Другой вариант, если вы решили использовать pytest в качестве тестового раннера, это pytest-benchmark . Эта библиотека может создать pytest fixture, называемые эталоном. Вы можете передать benchmark () любому вызываемому объекту, и он будет фиксировать время вызова с результатами pytest.
Вы можете установить pytest-benchmark из pip:
$ pip install pytest-benchmark
Затем вы можете добавить тест, который использует fixture:
def test_my_function(benchmark): result = benchmark(test)
Выполнение pytest теперь даст вам результаты теста:
Более подробная информация доступна на веб-сайте документации.
Еще один тест, который вы можете запустить в своем приложении, — это проверка на наличие распространенных ошибок безопасности или уязвимостей.
Вы можете установить bandit из PyPI, используя pip:
$ pip install bandit
Затем вы можете передать имя вашего модуля приложения с флагом -r, и он даст вам сводную информацию:
$ bandit -r my_sum [main] INFO profile include tests: None [main] INFO profile exclude tests: None [main] INFO cli include tests: None [main] INFO cli exclude tests: None [main] INFO running on Python 3.5.2 Run started:2018-10-08 00:35:02.669550 Test results: No issues identified. Code scanned: Total lines of code: 5 Total lines skipped (#nosec): 0 Run metrics: Total issues (by severity): Undefined: 0.0 Low: 0.0 Medium: 0.0 High: 0.0 Total issues (by confidence): Undefined: 0.0 Low: 0.0 Medium: 0.0 High: 0.0 Files skipped (0):
Как и в случае с flake8, правила для флагов bandit можно настраивать, и, если есть какие-либо, которые вы хотите игнорировать, вы можете добавить следующий раздел в файл setup.cfg с параметрами:
[bandit] exclude: /test tests: B101,B102,B301
Более подробная информация доступна на сайте GitHub.
Python сделал тестирование доступным благодаря встроенным командам и библиотекам, которые необходимы для проверки того, что ваши приложения работают так, как задумано. Начало работы с тестированием в Python не должно быть сложным: вы можете использовать unittest и писать небольшие поддерживаемые методы для проверки вашего кода.
По мере того, как вы узнаете больше о тестировании и расширении вашего приложения, вы можете рассмотреть возможность перехода на одну из других сред тестирования, таких как pytest, и начать использовать более продвинутые функции.
Спасибо за чтение.
Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…
Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…
Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…
Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…
Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…
Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…
View Comments
Добрый день, спасибо за перевод!
Ошибка в последнем слове в пункте "Почему они отличаются от других приложений", не "фао" а "фар".
Спасибо за комментарий
Опечатка
" Это быиблиотека может"
Спасибо за комментарий
Это самая лучшая статья в моей жизни. Любви и здоровья автору!