Пишете код на Python? Будет полезно знать о принципах тестирования Python-кода ваших приложений. Изучайте статью и применяйте навыки в работе.
Многие считают что язык программирования Python − это просто. Такое впечатление складывается после прочитанной книги по Python, статьи или видео-туториала. Возможно, он действительно проще, чем другие технологии, вот только без трудностей не бывает даже тут. Но и их можно избежать, если понять принципы тестирования Python-кода.
Как всё устроено
Сразу к делу. Вот как будет проходить проверка функции sum() (1,2,3) равна шести:
>>> assert sum([1, 2, 3]) == 6, "Should be 6"
Тест не выведет ничего на REPL, так как значения верны. Но если результат sum() неверен, это приведет к ошибке AssertionError и сообщению “Should be 6”.
>>> 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, потому что результат не соответствует 6. Переместите код в новый файл, названный 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
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() от (1,2,2) не равна 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
Для более масштабных вещей используют running tests. Это специальные приложения для запуска тестов, проверки вывода и предоставления инструментов для отладки и диагностики тестов и приложений.
Выбор Test Runner
Unittest
Unittest содержит как структуру тестирования Python, так и test runners. У него есть несколько требований:
- Нужно помещать свои тесты в классы как методы.
- Нужно использовать ряд специальных методов утверждения в unittest − TestCase вместо assert.
Для преобразования в unittest:
- Импортируйте его из стандартной библиотеки.
- Создайте класс TestSum, который наследуется от класса TestCase.
- Преобразуйте тестовые функции в методы путем добавления self в качестве первого аргумента.
- Изменить утверждение на использование метода self.assertEqual() в классе TestCase.
- Изменить точку входа в командной строке для вызова 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()
$ 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)
Nose
Совместим с любыми тестами, написанными с использованием unittest. Чтобы начать тестирование Python-кода, установите его из PyPl и выполните в командной строке. Он попытается обнаружить все скрипты с именем test*.py, наследующие от unittest.
$ 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)
Pytest
Pytest также поддерживает выполнение тестов unittest, а его преимущество заключается в написании своих тестов. Они представляют собой ряд функций в файле Python.
Кроме того, он отличается:
- Поддержкой встроенного утверждения 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"
Написание вашего первого теста
Если вы только начали изучать Python с нуля, обязательно затроньте и темы дебага/тестирования. Понимание принципов тестирования Python включает в себя принципы написания собственных тестов. Создайте новую папку проекта и внутри нее, под названием my_sum, еще одну. Внутри my_sum создайте пустой файл с именем __init__.py:
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, который будет содержать ваш первый тест:
project/
│
├── my_sum/
│ └── __init__.py
|
└── test.py
Как структурировать простой тест?
Прежде чем перейти к написанию тестов, вы должны понять следующее:
- Что вы хотите проверить?
- Вы пишете unit test или integration test?
После убедитесь, что структура теста соответствует следующему порядку:
- Создание структуры ввода.
- Выполнение кода и определение вывода.
- Сравнивание полученного с ожидаемым результатом.
Для этого приложения вы должны проверить sum(). Есть много вариантов поведения функции, которые нужно учитывать:
- Может ли функция суммировать целые числа?
- Может ли она использовать set или tuple?
- Что происходит, когда вы вводите неверное значение, например, переменную или целую строчку?
- Что происходит, когда значение отрицательно?
Начнем с суммы целых чисел.
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() будет:
- Описывать переменные списка чисел.
- Назначать результат my_sum.sum(data) для результирующей переменной.
- Проверять, что значение равно шести, используя метод .assertEqual() в классе unittestTestCase.
- Определять точку ввода в командную строку, где выполняется unittest test–runner .main().
Как писать утверждения и проверки assertions
Последним этапом теста является проверка вывода на основе известного ответа. Это называется утверждением − assertion. Есть несколько общих принципов их написания:
- Удостоверьтесь, что тесты могут повторяться.
- Попробуйте проверять результаты, которые относятся к входным данным, например, проверка результата суммы значений в sum().
Unittest поставляется со множеством методов для проверки значений и переменных. Вот некоторые из наиболее используемых:
Проверка Test Runners
if __name__ == '__main__':
unittest.main()
Это точка входа в командную строку. Она означает, что если вы выполните скрипт самостоятельно, запустив python.test.py в командной строке, он вызовет unittest.main(), после чего запустятся все классы, которые наследуются от unittest.TestCase в этом файле.
$ python -m unittest test
Вы можете предоставить дополнительные опции для изменения вывода. Один из них – “–v”:
$ python -m unittest -v test
test_list_int (test.TestSum) ... ok
----------------------------------------------------------------------
Ran 1 tests in 0.000s
Вместо предоставления имени модуля, содержащего тесты, можно запросить автоматическое обнаружение:
$ python -m unittest discover
Если у вас есть несколько тестов, и вы следуете шаблону test*.py, можно указать имя каталога, используя –s flag:
$ python -m unittest discover -s tests
Если исходный код отсутствует в корне каталога и содержится в подкаталоге, можно сообщить Unittest, где выполнить тесты, чтобы он правильно импортировал модули с –t flag:
$ python -m unittest discover -s tests -t src
Результаты тестирования
sum() должна иметь возможность принимать другие списки числовых типов (дроби).
В верхней части файла test.py добавьте оператор импорта:
from fractions import Fraction
Добавьте тест с утверждением, ожидающим неправильное значение. В этом случае ожидание sum() от (¼, ¼ и ⅖) будет равно 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)
Выполнение тестов в PyCharm
Если вы используете PyCharm IDE, вы можете запустить Unittest или pytest, выполнив следующие шаги:
- В окне инструментов проекта выберите каталог тестов
- В контекстном меню выберите команду запуск для Unittest.
Выполнение тестов из кода Visual Studio
Если у вас установлен плагин Python, вы можете настроить конфигурацию своих тестов, открыв командную палитру с помощью Ctrl+Shift+P и набрав «Python test»:
Выберите Debug All Unit Tests. VSCode выдаст подсказку для настройки тестовой среды. Нажмите на шестеренку, чтобы выбрать unittest и домашний каталог.
Тестирование для Django и Flask
Как использовать Django Test Runner
Шаблон startapp в Django создаст файл test.py внутри каталога приложений. Если его нет, создайте:
from django.test import TestCase
class MyTestCase(TestCase):
# Ваш метод
Основное отличие состоит в том, что наследовать нужно от django.test.TestCase вместо unittest.TestCase. Эти классы имеют один и тот же API, но Django TestCase устанавливает все необходимое для тестирования.
Чтобы выполнить свой тестовый пакет вместо использования unittest в командной строке, используйте метод manage.py:
$ python manage.py test
Если вы нуждаетесь в нескольких тестовых файлах, замените test.py на папку с именем test, поместите внутрь пустой файл с именем __init__.py и создайте файлы test_*.Py. Django обнаружит и выполнит их.
Как использовать unittest и Flask
Flask требует, чтобы приложение было импортировано и установлено в тестовом режиме. Можно создать копию тестового клиента и использовать его для запросов приложения.
Все экземпляры тестового клиента выполняются в методе setUp. В следующем примере my_app − имя приложения.
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
Сложные сценарии тестирования
Сбои
Ранее, когда мы делали список сценариев для проверки sum(), возник вопрос: что происходит при вводе неверного значения? Тест провалится.
Существует способ обработки ожидаемых ошибок. Можно использовать .assertRaises() в качестве контекстного менеджера, а затем выполнить тест внутри блока:
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. Позже условие можно будет изменить.
Структура
Существуют и побочные эффекты: они усложняют тестирование, поскольку при каждом выполнении результаты могут разниться.
Спасительные методы:
- Реструктурирование кода.
- Использование способа mocking для методов функции.
- Использование integration test вместо unit test.
Написание integration tests
До этого времени мы занимались в основном unit testing. Двигаемся дальше.
Integration testing – тестирование нескольких компонентов приложения для проверки их совместной работоспособности. Integration testing может требовать разные сценарии работы:
- Вызов HTTP REST API
- Вызов Python API
- Вызов веб–службы
- Запуск командной строки
Каждый из этих типов integration tests может быть записан так же, как и unit test. Существенное отличие состоит в том, что Integration tests проверяют сразу несколько компонентов. Можно разделить тесты на integration и unit − разбить их по папкам:
project/
│
├── my_app/
│ └── __init__.py
│
└── tests/
|
├── unit/
| ├── __init__.py
| └── test_sum.py
|
└── integration/
├── __init__.py
└── test_integration.py
Можно указать путь к тестам:
$ python -m unittest discover -s tests/integration
Тестирование data-driven приложений
Многие integration tests требуют базовые данные, содержащие определенные значения. Например, может потребоваться тест, который проверяет правильность отображения приложения с более чем 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 discovery будет выполнять их все. Для каждого набора тестовых данных может быть один тестовый пример:
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()
Тестирование в нескольких средах
До сих пор вы работали только с одной версией Python, используя виртуальную среду с определенным набором зависимостей. Tox − приложение, которое автоматизирует процесс тестирования Python в нескольких средах.
Установка Tox
$ pip install tox
Настройка Tox для ваших нужд
Tox настраивается через файл конфигурации в каталоге проекта. Он содержит следующее:
- Команда запуска для выполнения тестов
- Дополнительные пакеты, необходимые для выполнения
- Разные версии Python для тестирования
Вместо изучения синтаксиса конфигурации Tox, можно начать с использования приложения быстрого запуска:
$ tox-quickstart
Средство конфигурации Tox создаст файл, похожий на следующий в tox.ini:
[tox]
envlist = py27, py36
[testenv]
deps =
commands =
python -m unittest discover
Прежде чем запустить Tox, нужно создать файл setup.py, который будет содержать порядок установки пакета.
Вместо этого, можно добавить строку в файл tox.ini в заголовке [tox]:
[tox]
envlist = py27, py36
skipsdist=True
Если вы не будете создавать файл setup.py, но ваше приложение зависит от PyPl, вам нужно указать это в нескольких строках в разделе [testenv]. Например, для Django потребуется следующее:
[testenv]
deps = django
Теперь можно запустить Tox и создать две виртуальные среды: одну для Python 2.7 и одну для Python 3.6. Каталог Tox называется .tox/. Внутри него Tox выполнит обнаружение python – m unittest для каждой виртуальной среды.
Этот процесс также можно запустить, вызвав Tox в командной строке. На этом заканчиваем рассказ о принципах тестирования Python-кода.
Заключение
Python сделал тестирование доступным: unittest и собственные методы позволяют качественно тестировать код.
По мере развития навыков, можете постепенно перейти к использованию pytest и других более продвинутых функций.
Понравился материал об основах тестирования Python-кода? Возможно, вас заинтересует следующее:
Лучшие книги по Python:
- 13 лучших книг по Python для начинающих и продолжающих
- ТОП-10 книг по Python: эффективно, емко, доходчиво
Источник: Основы тестирования Python on Realpython
Комментарии