Пишете код на Python? Будет полезно знать о принципах тестирования Python-кода ваших приложений. Изучайте статью и применяйте навыки в работе.
Многие считают что Python − это просто. Возможно, это действительно проще, чем другие технологии, вот только без трудностей не бывает даже тут. Но и их можно избежать, если понять принципы тестирования Python-кода.
Как всё устроено
Сразу к делу. Вот как будет проходить проверка функции sum() (1,2,3) равна шести:
>>> assert sum([1, 2, 3]) == 6, «Should be 6»
1 | >>> 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
1234 | >>> 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»)
123456 | 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
12 | $ python test_sum.pyEverything 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»)
12345678910 | 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,3) не равна 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
1234567 | $ python test_sum_2.pyTraceback (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()
12345678910111213 | 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)
1234567891011121314 | $ 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)
123456789101112131415 | $ 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»
12345 | 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 включает в себя принципы написания собственных тестов. Создайте новую папку проекта и внутри нее, под названием my_sum, еще одну. Внутри my_sum создайте пустой файл с именем __init__.py:
project/
│
└── my_sum/
└── __init__.py
1234 | project/│└── my_sum/ └── __init__.py |
Откройте my_sum/__init__.py и создайте новую функцию sum(), которая обрабатывает повторения.
def sum(arg):
total = 0
for val in arg:
total += val
return total
12345 | 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
123456 | 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()
12345678910111213141516 | 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()
12 | if __name__ == ‘__main__’: unittest.main() |
Это точка входа в командную строку. Она означает, что если вы выполните скрипт самостоятельно, запустив python.test.py в командной строке, он вызовет unittest.main(), после чего запустятся все классы, которые наследуются от unittest.TestCase в этом файле.
$ python -m unittest test
1 | $ python -m unittest test |
Вы можете предоставить дополнительные опции для изменения вывода. Один из них – “–v”:
$ python -m unittest -v test
test_list_int (test.TestSum) … ok
———————————————————————-
Ran 1 tests in 0.000s
12345 | $ python -m unittest -v testtest_list_int (test.TestSum) … ok ———————————————————————-Ran 1 tests in 0.000s |
Вместо предоставления имени модуля, содержащего тесты, можно запросить автоматическое обнаружение:
$ python -m unittest discover
1 | $ python -m unittest discover |
Если у вас есть несколько тестов, и вы следуете шаблону test*.py, можно указать имя каталога, используя –s flag:
$ python -m unittest discover -s tests
1 | $ python -m unittest discover -s tests |
Если исходный код отсутствует в корне каталога и содержится в подкаталоге, можно сообщить Unittest, где выполнить тесты, чтобы он правильно импортировал модули с –t flag:
$ python -m unittest discover -s tests -t src
1 | $ python -m unittest discover -s tests -t src |
Результаты тестирования
sum() должна иметь возможность принимать другие списки числовых типов (дроби).
В верхней части файла test.py добавьте оператор импорта:
from fractions import Fraction
1 | 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()
123456789101112131415161718192021222324 | 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)
1234567891011121314 | $ python -m unittest testF.======================================================================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):
# Ваш метод
1234 | from django.test import TestCase class MyTestCase(TestCase): # Ваш метод |
Основное отличие состоит в том, что наследовать нужно от django.test.TestCase вместо unittest.TestCase. Эти классы имеют один и тот же API, но Django TestCase устанавливает все необходимое для тестирования.
Чтобы выполнить свой тестовый пакет вместо использования unittest в командной строке, используйте метод manage.py:
$ python manage.py test
1 | $ 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
12345678910111213 | import my_appimport 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()
1234567891011121314151617181920212223242526272829 | 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
1234567891011121314 | 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
1 | $ 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
12345678910111213141516171819 | 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()
1234567891011121314151617181920212223242526272829303132 | 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
1 | $ pip install tox |
Настройка Tox для ваших нужд
Tox настраивается через файл конфигурации в каталоге проекта. Он содержит следующее:
- Команда запуска для выполнения тестов
- Дополнительные пакеты, необходимые для выполнения
- Разные версии Python для тестирования
Вместо изучения синтаксиса конфигурации Tox, можно начать с использования приложения быстрого запуска:
$ tox-quickstart
1 | $ tox-quickstart |
Средство конфигурации Tox создаст файл, похожий на следующий в tox.ini:
[tox]
envlist = py27, py36
[testenv]
deps =
commands =
python -m unittest discover
12345678 | [tox]envlist = py27, py36 [testenv]deps = commands = python -m unittest discover |
Прежде чем запустить Tox, нужно создать файл setup.py, который будет содержать порядок установки пакета.
Вместо этого, можно добавить строку в файл tox.ini в заголовке [tox]:
[tox]
envlist = py27, py36
skipsdist=True
123 | [tox]envlist = py27, py36skipsdist=True |
Если вы не будете создавать файл setup.py, но ваше приложение зависит от PyPl, вам нужно указать это в нескольких строках в разделе [testenv]. Например, для Django потребуется следующее:
[testenv]
deps = django
12 | [testenv]deps = django |
Теперь можно запустить Tox и создать две виртуальные среды: одну для Python 2.7 и одну для Python 3.6. Каталог Tox называется .tox/. Внутри него Tox выполнит обнаружение python – m unittest для каждой виртуальной среды.
Этот процесс также можно запустить, вызвав Tox в командной строке. На этом заканчиваем рассказ о принципах тестирования Python-кода.
Заключение
Python сделал тестирование доступным: unittest и собственные методы позволяют качественно тестировать код.
По мере развития навыков, можете постепенно перейти к использованию pytest и других более продвинутых функций.
Понравился материал об основах тестирования Python-кода? Возможно, вас заинтересует следующее:
- Инструменты для анализа кода Python. Часть 1
- Инструменты для анализа кода Python. Часть 2
Источник: Основы тестирования Python on Realpytho