proglib
Сообщение

Почему специалистом по кибербезопасности быть интереснее, чем разработчиком или сисадмином? Приглашаем на вебинар от HackerU

Почему специалистом по кибербезопасности быть интереснее, чем разработчиком или сисадмином? Приглашаем на вебинар от HackerU

Погружаемся в основы и нюансы тестирования Python-кода

0
114464

Пишете код на Python? Будет полезно знать о принципах тестирования Python-кода ваших приложений. Изучайте статью и применяйте навыки в работе.

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

Как всё устроено

Сразу к делу. Вот как будет проходить проверка функции sum() (1,2,3) равна шести:

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

Тест не выведет ничего на REPL, так как значения верны. Но если результат sum() неверен, это приведет к ошибке AssertionError и сообщению “Should be 6”.

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

В REPL вы видите AssertionError, потому что результат не соответствует 6. Переместите код в новый файл, названный test_sum.py и выполните снова:

def test_sum():
    assert sum([1, 2, 3]) == 6, "Should be 6"

if __name__ == "__main__":
    test_sum()
    print("Everything passed")

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

$ python test_sum.py
Everything passed

sum() принимает любое повторяющееся значение в качестве первого аргумента. Вы проверили список, теперь проверьте так же и tuple. Создайте новый файл test_sum_2.py:

def test_sum():
    assert sum([1, 2, 3]) == 6, "Should be 6"

def test_sum_tuple():
    assert sum((1, 2, 2)) == 6, "Should be 6"

if __name__ == "__main__":
    test_sum()
    test_sum_tuple()
    print("Everything passed")

Когда вы выполняете test_sum_2.py, скрипт выдает ошибку, так как sum() от (1,2,2) не равна 6:

$ python test_sum_2.py
Traceback (most recent call last):
  File "test_sum_2.py", line 9, in <module>
    test_sum_tuple()
  File "test_sum_2.py", line 5, in test_sum_tuple
    assert sum((1, 2, 2)) == 6, "Should be 6"
AssertionError: Should be 6

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

Выбор Test Runner

Unittest

Unittest содержит как структуру тестирования Python, так и test runners. У него есть несколько требований:

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

Для преобразования в unittest:

  • Импортируйте его из стандартной библиотеки.
  • Создайте класс TestSum, который наследуется от класса TestCase.
  • Преобразуйте тестовые функции в методы путем добавления self в качестве первого аргумента.
  • Изменить утверждение на использование метода self.assertEqual() в классе TestCase.
  • Изменить точку входа в командной строке для вызова unittest.main().
  • Создайте test_sum_unittest.py:
import unittest


class TestSum(unittest.TestCase):

    def test_sum(self):
        self.assertEqual(sum([1, 2, 3]), 6, "Should be 6")

    def test_sum_tuple(self):
        self.assertEqual(sum((1, 2, 2)), 6, "Should be 6")

if __name__ == '__main__':
    unittest.main()
$ python test_sum_unittest.py
.F
======================================================================
FAIL: test_sum_tuple (__main__.TestSum)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_sum_unittest.py", line 9, in test_sum_tuple
    self.assertEqual(sum((1, 2, 2)), 6, "Should be 6")
AssertionError: Should be 6

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)

Nose

Совместим с любыми тестами, написанными с использованием unittest. Чтобы начать тестирование Python-кода, установите его из PyPl и выполните в командной строке. Он попытается обнаружить все скрипты с именем test*.py, наследующие от unittest.

$ pip install nose2
$ python -m nose2
.F
======================================================================
FAIL: test_sum_tuple (__main__.TestSum)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_sum_unittest.py", line 9, in test_sum_tuple
    self.assertEqual(sum((1, 2, 2)), 6, "Should be 6")
AssertionError: Should be 6

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)

Pytest

Pytest также поддерживает выполнение тестов unittest, а его преимущество заключается в написании своих тестов. Они представляют собой ряд функций в файле Python.

Кроме того, он отличается:

  • Поддержкой встроенного утверждения assert вместо использования специальных методов self.assert*().
  • Возможностью повторного запуска с пропущенного теста.
  • Наличием системы дополнительных плагинов.

Написание тестового примера TestSum для pytest будет выглядеть так:

def test_sum():
    assert sum([1, 2, 3]) == 6, "Should be 6"

def test_sum_tuple():
    assert sum((1, 2, 2)) == 6, "Should be 6"

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

Если вы только начали изучать Python с нуля, обязательно затроньте и темы дебага/тестирования. Понимание принципов тестирования Python включает в себя принципы написания собственных тестов. Создайте новую папку проекта и внутри нее, под названием my_sum, еще одну. Внутри my_sum создайте пустой файл с именем __init__.py:

project/
│
└── my_sum/
    └── __init__.py

Откройте my_sum/__init__.py и создайте новую функцию sum(), которая обрабатывает повторения.

def sum(arg):
    total = 0
    for val in arg:
        total += val
    return total

В этом коде создается переменная с именем total, которая повторяет все значения в arg и добавляет их к total.

Где писать тест

Создайте в корне файл test.py, который будет содержать ваш первый тест:

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

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

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

  • Что вы хотите проверить?
  • Вы пишете unit test или integration test?

После убедитесь, что структура теста соответствует следующему порядку:

  • Создание структуры ввода.
  • Выполнение кода и определение вывода.
  • Сравнивание полученного с ожидаемым результатом.

Для этого приложения вы должны проверить sum(). Есть много вариантов поведения функции, которые нужно учитывать:

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

Начнем с суммы целых чисел.

import unittest

from my_sum import sum


class TestSum(unittest.TestCase):
    def test_list_int(self):
        """
        Test that it can sum a list of integers
        """
        data = [1, 2, 3]
        result = sum(data)
        self.assertEqual(result, 6)

if __name__ == '__main__':
    unittest.main()

Код импортирует sum() из папки my_sum, затем определяет новый класс теста TestSum, наследуемый от unittest, а TestCase определяет тестовый метод .test_list_int() для проверки списка целых чисел.

Метод .test_list_int() будет:

  • Описывать переменные списка чисел.
  • Назначать результат my_sum.sum(data) для результирующей переменной.
  • Проверять, что значение равно шести, используя метод .assertEqual() в классе unittestTestCase.
  • Определять точку ввода в командную строку, где выполняется unittest test–runner .main().

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

Последним этапом теста является проверка вывода на основе известного ответа. Это называется утверждением − assertion. Есть несколько общих принципов их написания:

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

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

Проверка Test Runners

if __name__ == '__main__':
    unittest.main()

Это точка входа в командную строку. Она означает, что если вы выполните скрипт самостоятельно, запустив python.test.py в командной строке, он вызовет unittest.main(), после чего запустятся все классы, которые наследуются от unittest.TestCase в этом файле.

$ python -m unittest test

Вы можете предоставить дополнительные опции для изменения вывода. Один из них – “–v”:

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

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

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

$ python -m unittest discover

Если у вас есть несколько тестов, и вы следуете шаблону test*.py, можно указать имя каталога, используя –s flag:

$ python -m unittest discover -s tests

Если исходный код отсутствует в корне каталога и содержится в подкаталоге, можно сообщить Unittest, где выполнить тесты, чтобы он правильно импортировал модули с –t flag:

$ python -m unittest discover -s tests -t src

Результаты тестирования

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

В верхней части файла test.py добавьте оператор импорта:

from fractions import Fraction

Добавьте тест с утверждением, ожидающим неправильное значение. В этом случае ожидание sum() от (¼, ¼ и ⅖) будет равно 1.

import unittest

from my_sum import sum


class TestSum(unittest.TestCase):
    def test_list_int(self):
        """
        Test that it can sum a list of integers
        """
        data = [1, 2, 3]
        result = sum(data)
        self.assertEqual(result, 6)

    def test_list_fraction(self):
        """
        Test that it can sum a list of fractions
        """
        data = [Fraction(1, 4), Fraction(1, 4), Fraction(2, 5)]
        result = sum(data)
        self.assertEqual(result, 1)

if __name__ == '__main__':
    unittest.main()

Если вы снова выполните тест с python –m unittest test, вы увидите следующее:

$ python -m unittest test
F.
======================================================================
FAIL: test_list_fraction (test.TestSum)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test.py", line 21, in test_list_fraction
    self.assertEqual(result, 1)
AssertionError: Fraction(9, 10) != 1

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)

Выполнение тестов в PyCharm

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

  • В окне инструментов проекта выберите каталог тестов
  • В контекстном меню выберите команду запуск для Unittest.

PyCharm Testing

Выполнение тестов из кода Visual Studio

Если у вас установлен плагин Python, вы можете настроить конфигурацию своих тестов, открыв командную палитру с помощью Ctrl+Shift+P и набрав «Python test»:Visual Studio Code Step 1

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

Visual Studio Code Step 2

Тестирование для Django и Flask

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

Шаблон startapp в Django создаст файл test.py внутри каталога приложений. Если его нет, создайте:

from django.test import TestCase

class MyTestCase(TestCase):
    # Ваш метод

Основное отличие состоит в том, что наследовать нужно от django.test.TestCase вместо unittest.TestCase. Эти классы имеют один и тот же API, но Django TestCase устанавливает все необходимое для тестирования.

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

$ python manage.py test

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

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

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

Все экземпляры тестового клиента выполняются в методе setUp. В следующем примере my_app − имя приложения.

import my_app
import unittest


class MyTestCase(unittest.TestCase):

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

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

Сложные сценарии тестирования

Сбои

Ранее, когда мы делали список сценариев для проверки sum(), возник вопрос: что происходит при вводе неверного значения? Тест провалится.

Существует способ обработки ожидаемых ошибок. Можно использовать .assertRaises() в качестве контекстного менеджера, а затем выполнить тест внутри блока:

import unittest

from my_sum import sum


class TestSum(unittest.TestCase):
    def test_list_int(self):
        """
        Test that it can sum a list of integers
        """
        data = [1, 2, 3]
        result = sum(data)
        self.assertEqual(result, 6)

    def test_list_fraction(self):
        """
        Test that it can sum a list of fractions
        """
        data = [Fraction(1, 4), Fraction(1, 4), Fraction(2, 5)]
        result = sum(data)
        self.assertEqual(result, 1)

    def test_bad_type(self):
        data = "banana"
        with self.assertRaises(TypeError):
            result = sum(data)

if __name__ == '__main__':
    unittest.main()

Теперь этот тест будет пройден только если sum(data) вызовет TypeError. Позже условие можно будет изменить.

Структура

Существуют и побочные эффекты: они усложняют тестирование, поскольку при каждом выполнении результаты могут разниться.

Основы тестирования Python

Спасительные методы:

  • Реструктурирование кода.
  • Использование способа mocking для методов функции.
  • Использование integration test вместо unit test.

Написание integration tests

До этого времени мы занимались в основном unit testing. Двигаемся дальше.

Integration testing – тестирование нескольких компонентов приложения для проверки их совместной работоспособности. Integration testing может требовать разные сценарии работы:

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

Каждый из этих типов integration tests может быть записан так же, как и unit test. Существенное отличие состоит в том, что Integration tests проверяют сразу несколько компонентов. Можно разделить тесты на integration и unit − разбить их по папкам:

project/
│
├── my_app/
│   └── __init__.py
│
└── tests/
    |
    ├── unit/
    |   ├── __init__.py
    |   └── test_sum.py
    |
    └── integration/
        ├── __init__.py
        └── test_integration.py

Можно указать путь к тестам:

$ python -m unittest discover -s tests/integration

Тестирование data-driven приложений

Многие integration tests требуют базовые данные, содержащие определенные значения. Например, может потребоваться тест, который проверяет правильность отображения приложения с более чем 100 клиентами в базе данных, написанной на японском.

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

Вот пример этой структуры, если данные состоят из файлов JSON:

project/
│
├── my_app/
│   └── __init__.py
│
└── tests/
    |
    └── unit/
    |   ├── __init__.py
    |   └── test_sum.py
    |
    └── integration/
        |
        ├── fixtures/
        |   ├── test_basic.json
        |   └── test_complex.json
        |
        ├── __init__.py
        └── test_integration.py

В тесте можно использовать метод .setUp() для загрузки тестовых данных из файла. Помните, что у вас может быть несколько тестов в одном файле Python, и unittest discovery будет выполнять их все. Для каждого набора тестовых данных может быть один тестовый пример:

import unittest


class TestBasic(unittest.TestCase):
    def setUp(self):
        # Load test data
        self.app = App(database='fixtures/test_basic.json')

    def test_customer_count(self):
        self.assertEqual(len(self.app.customers), 100)

    def test_existence_of_customer(self):
        customer = self.app.get_customer(id=10)
        self.assertEqual(customer.name, "Org XYZ")
        self.assertEqual(customer.address, "10 Red Road, Reading")


class TestComplexData(unittest.TestCase):
    def setUp(self):
        # load test data
        self.app = App(database='fixtures/test_complex.json')

    def test_customer_count(self):
        self.assertEqual(len(self.app.customers), 10000)

    def test_existence_of_customer(self):
        customer = self.app.get_customer(id=9999)
        self.assertEqual(customer.name, u"バナナ")
        self.assertEqual(customer.address, "10 Red Road, Akihabara, Tokyo")

if __name__ == '__main__':
    unittest.main()

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

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

Установка Tox

$ pip install tox

Настройка Tox для ваших нужд

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

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

Вместо изучения синтаксиса конфигурации Tox, можно начать с использования приложения быстрого запуска:

$ tox-quickstart

Средство конфигурации Tox создаст файл, похожий на следующий в tox.ini:

[tox]
envlist = py27, py36

[testenv]
deps =

commands =
    python -m unittest discover

Прежде чем запустить Tox, нужно создать файл setup.py, который будет содержать порядок установки пакета.

Вместо этого, можно добавить строку в файл tox.ini в заголовке [tox]:

[tox]
envlist = py27, py36
skipsdist=True

Если вы не будете создавать файл setup.py, но ваше приложение зависит от PyPl, вам нужно указать это в нескольких строках в разделе [testenv]. Например, для Django потребуется следующее:

[testenv]
deps = django

Теперь можно запустить Tox и создать две виртуальные среды: одну для Python 2.7 и одну для Python 3.6. Каталог Tox называется .tox/. Внутри него Tox выполнит обнаружение python – m unittest для каждой виртуальной среды.

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

Заключение

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

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

Понравился материал об основах тестирования Python-кода? Возможно, вас заинтересует следующее:

Лучшие книги по Python:

Источник: Основы тестирования Python on Realpython

РУБРИКИ В СТАТЬЕ

МЕРОПРИЯТИЯ

Комментарии 0

ВАКАНСИИ

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

BUG