Введение в тестирование в Python

Spread the love

Перевод статьи  Anthony Shaw: Getting Started With Testing in Python Статья будет интересна тем кто еще мало знаком с тестированием в Python и быстро получить обзорные знания для дальнейшего изучения.

Это руководство предназначено для тех, кто уже имеет опыт написания приложений в Python, но еще не написал ни одного теста.

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

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

Тестирование вашего кода

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

Автоматизированное и ручное тестирование

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

Исследовательское тестирование — это форма тестирования, которая проводится без плана. В таком виде тестирования вы просто изучаете приложение.

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

Это не похоже на веселье, не так ли?

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

Модульные (юнит) тесты против интеграционных тестов

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

Подумайте, как вы можете проверить свет в автомобиле. Вы должны включить свет (это будет называться один шаг теста (test step)), далее нужно выйти из машины или попросить друга проверить, включены ли огни (это называется утверждение теста (test assertion)). Тестирование нескольких компонентов называется интеграционным тестированием (integration testing). Основная проблема с интеграционным тестированием — это когда интеграционный тест не дает правильного результата. Иногда очень трудно диагностировать проблему, не имея возможности определить, какая часть системы вышла из строя. Если свет не включился, то, возможно сломаны лампы или разряжен аккумулятор. А как насчет генератора? Или может быть сломан компьютер машины?

Если у вас модный современный автомобиль, он сообщит вам, когда ваши лампочки вышли из строя. Это делается с помощью модульных тестов (unit test).

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

Вы только что рассмотрели два типа тестов:

  1. Интеграционный тест, который проверяет, что компоненты в вашем приложении правильно работают друг с другом.
  2. Модульный тест (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 — это специальное приложение, предназначенное для запуска тестов, проверки выходных данных и предоставления инструментов для отладки и диагностики тестов и приложений.

Выбор a Test Runner

В Python доступно множество тестовых раннеров. Встроенный в стандартную библиотеку Python, называется unittest. В этом руководстве мы будем использовать тестовые примеры на unittest. Принципы unittest легко переносимы на другие тестовые фреймворки. Три самых популярных test runner:

  • unittest
  • nose или nose2
  • pytest

unittest

unittest был встроен в стандартную библиотеку Python начиная с версии 2.1. Вы, вероятно, могли видеть его использование в коммерческих приложениях Python и проектах с открытым исходным кодом.

unittest содержит как тестовую среду, так и test runner. У unittest есть некоторые важные требования для написания и выполнения тестов.

unittest требует, чтобы:

  • Вы помещаете свои тесты в методы класса unittest.TestCase
  • Вы используете ряд специальных методов утверждения класса unittest.TestCase вместо встроенного оператора assert

Чтобы преобразовать предыдущий пример в тестовый блок unittest, вам необходимо:

  1. Импортировать unittest из стандартной библиотеки
  2. Создайте класс с именем TestSum, который наследуется от класса TestCase
  3. Преобразуйте тестовые функции в методы, добавив self в качестве первого аргумента
  4. Измените утверждение для использования метода self.assertEqual()
  5. Изменить точку входа командной строки для вызова 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()

Далее запустим этот файл в командной строке, и мы должны увидеть один успех тест (обозначенный . ) и один сбой (обозначенный 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.

nose

Вы можете обнаружить, что когда вы напишете сотни или даже тысячи тестов для своего приложения, будет становится все труднее понять и использовать результаты 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

pytest так же поддерживает созданные тесты с unittest. Настоящее преимущество pytest заключается в написании test case. Test case в pytest представляют собой серию функций в файле Python, начинающихся с имени test_.

У pytest есть и другие замечательные возможности:

  • Поддержка встроенного оператора assert вместо использования специальных методов self.assert
  • Поддержка фильтрации для test case
  • Возможность перезапуска с последнего неудачного теста
  • Экосистема из сотен плагинов для расширения функциональности

Написание теста 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.

Из чего состоит простой тест

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

  1. Что вы хотите проверить?
  2. Вы пишете юнит-тест или интеграционный тест?

Далее структура теста должна следовать этому рабочему процессу:

  1. Определитесь и создайте входные данные
  2. Осуществите проверку
  3. Сравните результат с ожидаемым результатом

В нашем приложение sum() вы можете проверить множество вариантов поведения, таких как:

  • Можно ли суммировать список целых чисел?
  • Можно ли суммировать tuple или set?
  • Можно ли суммировать список floats?
  • Что происходит, когда вы указываете неверное значение, такое как одно целое число или строка?
  • Что происходит, когда одно из значений является отрицательным?

Самый простой тест — это список целых чисел. Создайте файл 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()

Этот пример кода:

  1. Импортирует sum() из созданного вами пакета my_sum
  2. Определяет новый класс с именем TestSum, который наследуется от unittest.TestCase
  3. Определяет метод теста .test_list_int (), чтобы проверить список целых чисел. Метод .test_list_int() в котором:
    • Объявляется переменная data со списком чисел (1, 2, 3)
    • Присваивается результат my_sum.sum(data) переменной result
    • Assert Утверждается, что значение результата равно 6, используя метод .assertEqual() класса unittest.TestCase
  4. Определяет точку входа c командной строки, в которой запускается тестовый модуль unittest.main()

Если вы не уверены, что такое self или как определяется .assertEqual(), вы можете освежить свое объектно-ориентированное программирование с помощью объектно-ориентированного программирования Python 3.

Как писать утверждения Assertions

Последний шаг написания теста — проверка вывода по известному ответу. Это известно как утверждение (assertion). Существует несколько общих рекомендаций по написанию утверждений:

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

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

MethodEquivalent 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)

В выходных данных мы видим следующую информацию:

  1. Первая строка показывает результаты выполнения всех тестов, один тест провал (F) и один пройден ( . ).
  2. Запись FAIL показывает некоторые подробности о неудачном тесте:
    • Имя метода теста (test_list_fraction)
    • Тестовый модуль (test) и тестовый класс (TestSum)
    • Фрагмент кода где возникла ошибка
    • Детали тестового утверждения с ожидаемым результатом (1) и фактическим результатом (Fraction(9, 10))

Помните, что вы можете вывести дополнительную информацию в результатах теста, добавив флаг -v к команде python -m unittest.

Запуск тестов из PyCharm

Если вы используете PyCharm IDE, вы можете запустить unittest или pytest, выполнив следующие действия:

  1. В окне инструмента Project tool выберите каталог tests.
  2. В контекстном меню выберите команду запуска для unittest. Например, выберите «Выполнить юнит-тесты в моих тестах…» (Unittests in my Tests…).

Эта команда выполнит unittest в тестовом окне и выдаст вам результаты в PyCharm:

Более подробная информация доступна на веб-сайте PyCharm.

Запуск тестов в Visual Studio

Если вы используете 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, существуют некоторые важные различия в том, как вы пишете и запускаете тесты.

Почему они отличаются от других приложений

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

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

Django и Flask упрощают эту задачу, предоставляя среду тестирования на основе unittest.

Как использовать Django Test Runner

Шаблон 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.

Как использовать unittest во Flask

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

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

Код в вашем тестовом файле должен выглядеть следующим образом:

import my_app
import unittest

class MyTestCase(unittest.TestCase):

    def setUp(self):
        my_app.app.testing = True
        self.app = my_app.app.test_client()

    def test_home(self):
        result = self.app.get('/')
        # Make your assertions

Затем вы можете выполнить контрольные примеры с помощью команды python -m unittest discover.

Более подробная информация доступна на веб-сайте документации Flask.

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

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

  1. Создайте входные данные для теста
  2. Выполнить тестирование
  3. Сравните результат с ожидаемым результатом

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

Данные, которые создаются в качестве входных данных, известны как 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 любым типом исключения, который вы выберете.

Изоляция поведения в вашем приложении

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

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

  • Рефакторинг кода в соответствии с принципом единой ответственности
  • Mock (фейковый) вызов метода или функции для устранения побочных эффектов
  • Использование интеграционного тестирования вместо модульного тестирования для выбранной части приложения

Если вы не знакомы с mocking, почитайте Python CLI Testing, чтобы найти отличные примеры.

Написание интеграционных тестов

До сих пор мы рассказывали главным образом о модульном тестирование. Модульное тестирование — отличный способ создать предсказуемый и стабильный код.

Интеграционное тестирование — это тестирование нескольких компонентов приложения для проверки их совместной работы. Интеграционное тестирование может потребовать от пользователя или пользователя приложения:

  • Вызов HTTP REST API
  • Вызов Python API
  • Вызов веб сервисов
  • Запуск командной строки

Каждый из этих типов интеграционных тестов может быть написан так же, как модульный тест, следуя шаблонам 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.

Testing Data-Driven Applications

Для многих интеграционных тестов требуются данные бэкэнда, такие как база данных, с определенными значениями. Например, вам может потребоваться тест, который проверяет, правильно ли отображается приложение с более чем 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

Tox доступен в PyPI в виде пакета для установки через pip:

$ pip install tox

Теперь, когда у вас установлен Tox, его необходимо настроить.

Настройка Tox для ваших зависимостей

Tox настраивается через файл конфигурации в каталоге вашего проекта. Файл конфигурации Tox содержит следующее:

  • Команда для запуска выполнения тестов
  • Любые дополнительные пакеты, необходимые перед выполнением
  • Целевые версии Python для тестирования

Вместо того, чтобы изучать синтаксис конфигурации 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

Вывод 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:

  1. Протестировать в Python 2.7 и 3.7 (вы можете заменить эти версии на любую по вашему выбору.)
  2. Установить все пакеты, которые вы перечислили в файле require.txt (вы должны удалить этот раздел, если у вас нет никаких зависимостей.)
  3. Запустить python -m unittest discover для запуска тестов.

После того как вы закоммите и отправите этот файл, Travis CI будет запускать эти команды каждый раз, когда вы будете пушить в удаленный репозиторий Git.

Что дальше

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

Введение линтеров в ваше приложение

Tox и Travis CI имеют конфигурацию для команд запуска тестирования. Команда тестирования, которую вы использовали в этом руководстве, — это python -m unittest discover.

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

Один из таких типов приложений называется линтер (linter). Линтер проверяет ваш код и комментирует его. Он может дать вам советы об ошибках, которые вы сделали, исправить пробелы и даже предсказать ошибки, которые вы, возможно, внесли.

Для получения дополнительной информации о линтерах прочитайте руководство по качеству кода Python.

Пассивный линтинг с flake8

Популярный линтер, который комментирует стиль вашего кода по отношению к спецификации 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, и начать использовать более продвинутые функции.

Спасибо за чтение.

Была ли вам полезна эта статья?
[24 / 4.8]

Spread the love
Подписаться
Уведомление о
guest
5 Комментарий
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Андрей
Андрей
5 лет назад

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

Антон
Антон
4 лет назад

Опечатка
» Это быиблиотека может»

edteam
Администратор
4 лет назад
Reply to  Антон

Спасибо за комментарий

Андрей
Андрей
3 лет назад

Это самая лучшая статья в моей жизни. Любви и здоровья автору!