Начало работы с тестированием в Python

Начало работы с тестированием в Python

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

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

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

Free Bonus:5 Thoughts On Python Mastery, бесплатный курс для разработчиков Python, который показывает вам план действий и образ мышления, который вам понадобится, чтобы вывести свои навыки Python на новый уровень.

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

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

Автоматизированный против Ручное тестирование

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

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

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

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

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

Модульные тесты против Интеграционные тесты

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

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

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

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

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

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

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

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

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

Вы можете написать как интеграционные, так и модульные тесты на Python. Чтобы написать модульный тест для встроенной функцииsum(), вы должны сравнить выводsum() с известным выводом.

Например, вот как вы проверяете, чтоsum() чисел(1, 2, 3) равно6:

>>>

>>> assert sum([1, 2, 3]) == 6, "Should be 6"

Это не будет ничего выводить на REPL, потому что значения верны.

Если результатsum() неверен, это приведет к ошибкеAssertionError и сообщению"Should be 6". Повторите попытку утверждения утверждения с неправильными значениями, чтобы увидетьAssertionError:

>>>

>>> assert sum([1, 1, 1]) == 6, "Should be 6"
Traceback (most recent call last):
  File "", line 1, in 
AssertionError: Should be 6

В REPL вы видите повышенныйAssertionError, потому что результатsum() не соответствует6.

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

$ python test_sum.py
Everything passed

Вы можете увидеть успешный результатEverything passed.

В Pythonsum() принимает любую итерацию в качестве первого аргумента. Вы проверяли со списком. Теперь протестируйте также и с кортежем. Создайте новый файл с именем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) равно5, а не6. Результат сценария дает вам сообщение об ошибке, строку кода и обратную трассировку:

$ python test_sum_2.py
Traceback (most recent call last):
  File "test_sum_2.py", line 9, in 
    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

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

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

Выбор тестового бегуна

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

  • unittest

  • nose илиnose2

  • pytest

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

unittest

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

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

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

  • Вы помещаете свои тесты в классы как методы

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

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

  1. Импортироватьunittest из стандартной библиотеки

  2. Создайте класс с именемTestSum, который наследуется от классаTestCase

  3. Преобразуйте тестовые функции в методы, добавивself в качестве первого аргумента

  4. Измените утверждения, чтобы использовать методself.assertEqual() в классеTestCase

  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)

Вы только что выполнили два теста, используя средство запуска тестовunittest.

Note: Будьте осторожны, если вы пишете тестовые примеры, которые необходимо выполнять как в Python 2, так и в 3. В Python 2.7 и нижеunittest называетсяunittest2. Если вы просто импортируете изunittest, вы получите разные версии с разными функциями между Python 2 и 3.

Для получения дополнительной информации оunittest, вы можете изучитьunittest Documentation.

nose

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

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

Чтобы начать работу сnose2, установите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)

Вы только что выполнили тест, созданный вами вtest_sum_unittest.py из программы запуска тестовnose2. nose2 предлагает множество флагов командной строки для фильтрации выполняемых вами тестов. Для получения дополнительной информации вы можете изучитьNose 2 documentation.

pytest

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

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

  • Поддержка встроенного оператора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"

Вы отказались отTestCase, использования классов и точки входа в командную строку.

Более подробную информацию можно найти наPytest Documentation Website.

Написание вашего первого теста

Давайте объединим все, что вы уже узнали, и вместо тестирования встроенной функцииsum() протестируем простую реализацию того же требования.

Создайте новую папку проекта и внутри нее новую папку с именемmy_sum. Внутриmy_sum создайте пустой файл с именем__init__.py. Создание файла__init__.py означает, что папкуmy_sum можно импортировать как модуль из родительского каталога.

Папка вашего проекта должна выглядеть так:

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

project/
│
├── my_sum/
│   └── __init__.py
|
└── test.py

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

Note: Что, если ваше приложение представляет собой один скрипт?

Вы можете импортировать любые атрибуты сценария, такие как классы, функции и переменные, используя встроенную функцию__import__(). Вместоfrom my_sum import sum можно написать следующее:

target = __import__("my_sum.py")
sum = target.sum

Преимущество использования__import__() заключается в том, что вам не нужно превращать папку проекта в пакет, и вы можете указать имя файла. Это также полезно, если ваше имя файла конфликтует с какими-либо стандартными пакетами библиотеки. Например,math.py столкнется с модулемmath.

Как структурировать простой тест

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

  1. Что вы хотите проверить?

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

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

  1. Создайте свой вклад

  2. Выполните тестируемый код, захватив вывод

  3. Сравните результат с ожидаемым результатом

Для этого приложения вы тестируетеsum(). Вы можете проверить множество вариантов поведения в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()

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

  1. Импортируетsum() из созданного вами пакетаmy_sum

  2. Определяет новый класс тестового примера с именемTestSum, который наследуется отunittest.TestCase

  3. Определяет тестовый метод.test_list_int() для проверки списка целых чисел. Метод.test_list_int():

    • Объявите переменнуюdata со списком чисел(1, 2, 3)

    • Присвойте результатmy_sum.sum(data) переменнойresult

    • Утвердите, что значениеresult равно6, используя метод.assertEqual() в классеunittest.TestCase

  4. Определяет точку входа в командную строку, которая запускает средство выполнения тестовunittest.main()

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

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

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

  • Убедитесь, что тесты повторяемы, и запустите тест несколько раз, чтобы убедиться, что он дает один и тот же результат каждый раз

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

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

метод Эквивалентно

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

Если вы обнаружите, что блок кода, который вы хотите протестировать, имеет множество побочных эффектов, возможно, вы нарушаетеSingle Responsibility Principle. Нарушение принципа единой ответственности означает, что фрагмент кода выполняет слишком много задач, и было бы лучше, если бы он подвергся рефакторингу. Следование принципу единой ответственности является отличным способом разработки кода, позволяющего легко создавать повторяемые и простые модульные тесты и, в конечном счете, надежные приложения.

Выполнение вашего первого теста

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

Выполнение тестовых бегунов

Приложение 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

Это запустит тот же тестовый модуль (называемыйtest) через командную строку.

Вы можете предоставить дополнительные параметры для изменения вывода. Один из них --v для подробностей. Попробуйте следующее:

$ python -m unittest -v test
test_list_int (test.TestSum) ... ok

----------------------------------------------------------------------
Ran 1 tests in 0.000s

Это выполнило один тест внутриtest.py и распечатало результаты на консоли. В режиме 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/, просканирует все файлыtest*.py внутри каталогаtests и выполнит их.

Понимание результатов теста

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

sum() должен иметь возможность принимать другие списки числовых типов, например дроби.

Вверху файлаtest.py добавьте оператор импорта для импорта типа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. В окне инструмента "Проект" выберите каталогtests.

  2. В контекстном меню выберите команду запуска дляunittest. Например, выберитеRun ‘Unittests in my Tests…’.

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

PyCharm Testing

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

Запуск ваших тестов из кода Visual Studio

Если вы используете IDE Microsoft Visual Studio Code, поддержка выполненияunittest,nose иpytest встроена в подключаемый модуль Python.

Если у вас установлен плагин Python, вы можете настроить конфигурацию своих тестов, открыв палитру команд с помощьюCtrl[.kbd .key-shift]##Shift##[.kbd .key-p]#P # и набрав «Python test». Вы увидите ряд вариантов:

Visual Studio Code Step 1

ВыберитеDebug All Unit Tests, и VSCode выдаст запрос на настройку тестовой среды. Щелкните шестеренку, чтобы выбрать средство запуска тестов (unittest) и домашний каталог (.).

После настройки вы увидите статус ваших тестов в нижней части окна, и вы можете быстро получить доступ к журналам тестов и снова запустить тесты, нажав на следующие значки:

Visual Studio Code Step 2

Это показывает, что тесты выполняются, но некоторые из них не проходят.

Тестирование для веб-фреймворков, таких как Django и Flask

Если вы пишете тесты для веб-приложения, используя одну из популярных сред, таких как Django или Flask, существуют некоторые важные различия в том, как вы пишете и запускаете тесты.

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

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

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

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

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

Шаблон Djangostartapp создаст файлtests.py внутри каталога вашего приложения. Если у вас его еще нет, вы можете создать его со следующим содержимым:

from django.test import TestCase

class MyTestCase(TestCase):
    # Your test methods

Основное различие с примерами до сих пор состоит в том, что вам нужно наследовать отdjango.test.TestCase вместоunittest.TestCase. У этих классов один и тот же API, но класс DjangoTestCase настраивает все необходимые состояния для тестирования.

Чтобы выполнить свой набор тестов, вместо использованияunittest в командной строке вы используетеmanage.py test:

$ python manage.py test

Если вам нужно несколько тестовых файлов, заменитеtests.py папкой с именемtests, вставьте внутрь пустой файл с именем__init__.py и создайте свои файлыtest_*.py. Джанго обнаружит и выполнит их.

Более подробная информация доступна наDjango Documentation Website.

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

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

Вся инстанциация тестового клиента выполняется методомsetUp вашего тестового примера. В следующем примереmy_app - это имя приложения. Не волнуйтесь, если вы не знаете, что делаетsetUp. Вы узнаете об этом в разделеMore Advanced Testing Scenarios.

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

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 Documentation Website.

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

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

  1. Создайте свой вклад

  2. Выполнить код, захватив вывод

  3. Сравните результат с ожидаемым результатом

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

Данные, которые вы создаете в качестве входных данных, известны какfixture. Обычной практикой является создание светильников и их повторное использование.

Если вы запускаете один и тот же тест, каждый раз передаете разные значения и ожидаете одного и того же результата, это называетсяparameterization.

Обработка ожидаемых сбоев

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

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

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

Testing Side Effects

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

  • Рефакторинг кода в соответствии с принципом единой ответственности

  • Макет любого вызова метода или функции для устранения побочных эффектов

  • Использование интеграционного тестирования вместо модульного тестирования для этой части приложения

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

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

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

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

  • Вызов HTTP REST API

  • Вызов API Python

  • Вызов веб-сервиса

  • Запуск командной строки

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

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

В библиотекеrequests есть дополнительный пакет под названиемresponses, который дает вам способы создавать фикстуры ответов и сохранять их в ваших тестовых папках. Узнайте большеon their GitHub Page.

Тестирование в нескольких средах

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

Установка Tox

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

$ pip install 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 в папке вашего приложения, содержащий шаги по установке вашего пакета. Если у вас его нет, вы можете проследитьthis guide о том, как создать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 discover для каждой виртуальной среды.

Вы можете запустить этот процесс, вызвав Tox в командной строке:

$ tox

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

Выполнение Tox

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

Есть несколько дополнительных параметров командной строки, которые приятно запомнить.

Запустите только одну среду, такую ​​как Python 3.6:

$ tox -e py36

Воссоздайте виртуальные среды, если ваши зависимости изменились илиsite-packages поврежден:

$ tox -r

Запустите Tox с меньшим количеством подробных выводов:

$ tox -q

Запуск Tox с более подробным выводом:

$ tox -v

Более подробную информацию о Tox можно найти наTox Documentation Website.

Автоматизация выполнения ваших тестов

До сих пор вы выполняли тесты вручную, выполнив команду. Существуют некоторые инструменты для автоматического выполнения тестов, когда вы вносите изменения и фиксируете их в репозитории системы управления версиями, такой как Git. Инструменты автоматического тестирования часто называют инструментами CI / CD, что означает «Непрерывная интеграция / Непрерывное развертывание». Они могут запускать ваши тесты, компилировать и публиковать любые приложения и даже развертывать их в рабочей среде.

Travis CI - одна из многих доступных служб непрерывной интеграции (CI).

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. Установите все пакеты, которые вы указали вrequirements.txt (вам следует удалить этот раздел, если у вас нет зависимостей).

  3. Запуститеpython -m unittest discover, чтобы запустить тесты

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

Что дальше

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

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

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

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

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

Для получения дополнительной информации о линтерах прочтитеPython Code Quality tutorial.

Пассивный линтинг с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

Полный список параметров конфигурации доступен наDocumentation Website.

Теперь вы можете добавитьflake8 в конфигурацию CI. Для Travis CI это будет выглядеть следующим образом:

matrix:
  include:
    - python: "2.7"
      script: "flake8"

Трэвис прочитает конфигурацию в.flake8 и не выполнит сборку, если возникнут какие-либо ошибки линтинга. Обязательно добавьте зависимостьflake8 в ваш файлrequirements.txt.

Агрессивный Linting с форматером кода

flake8 - пассивный линтер: он рекомендует изменения, но вам нужно пойти и изменить код. Более агрессивный подход - это средство форматирования кода. Форматировщики кода автоматически изменят ваш код в соответствии с практикой стилей и макетов.

black - очень неумолимая программа форматирования. У него нет опций конфигурации, и у него очень специфический стиль. Это делает его отличным инструментом для вставки в ваш тестовый конвейер.

Note:black требует Python 3.6+.

Вы можете установитьblack через pip:

$ pip install black

Затем, чтобы запуститьblack в командной строке, укажите файл или каталог, который вы хотите отформатировать:

$ black test.py

Сохранение вашего кода теста в чистоте

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

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

При написании тестов старайтесь следовать принципуDRY:Don’tRepeatYourself.

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

$ flake8 --max-line-length=120 tests/

Тестирование на снижение производительности между изменениями

Есть много способов для тестирования кода в Python. Стандартная библиотека предоставляет модульtimeit, который может задавать время функций несколько раз и давать вам распределение. Этот пример выполнитtest() 100 раз иprint() на выходе:

def test():
    # ... your code

if __name__ == '__main__':
    import timeit
    print(timeit.timeit("test()", setup="from __main__ import test", number=100))

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

Вы можете установитьpytest-benchmark из PyPI, используяpip:

$ pip install pytest-benchmark

Затем вы можете добавить тест, который использует прибор и передает вызываемый для выполнения:

def test_my_function(benchmark):
    result = benchmark(test)

Выполнениеpytest теперь даст вам результаты теста:

Pytest benchmark screenshot

Более подробная информация доступна наDocumentation Website.

Тестирование на недостатки безопасности в вашем приложении

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

Вы можете установить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 Website.

Заключение

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

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

Спасибо за чтение. Я надеюсь, что у вас есть будущее без ошибок с Python!

Related