Краткое руководство о том, как улучшить качество кода и душевное спокойствие

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

Что такое модульный тест?

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

Чтобы проиллюстрировать различные сценарии, мы будем использовать следующую функцию:

Как написать тест?

У Python есть мощный пакет для модульных тестов: «unittest». Есть несколько других, у которых есть множество плюсов и минусов, которые нужно оценить. Но помните, у нас нет времени на испытания. Вы никогда не ошибетесь с unittest, поэтому мы будем использовать его в наших примерах.

Вот так выглядит модульный тест на Python:

Единственное обязательное условие здесь - это то, что ваши тесты должны быть методами внутри класса, наследующего TestCase. Все ваши тесты должны быть методами с именами, начинающимися с «test_». Все остальное будет пропущено.

А из командной строки вы запускаете все файлы модульного теста следующим образом:

python -m unittest discover

Эти расширенные параметры, которые могут дать вам дополнительный контроль над тем, что выполняется:

#specify what modules to be executed
python -m unittest test_module1 test_module2
#execute all tests inside a specific class
python -m unittest test_module.TestClass
#cherry pick a test to be executed
python -m unittest test_module.TestClass.test_method

Удалить все зависимости

Первый шаг - определить все зависимости вашей функции. В нашей функции подопытных кроликов мы можем идентифицировать 2 внешних конечных точки. Первый - это то, где данные выбираются для обработки, а другой - куда отправляется результат. Та же самая логика будет применяться к файлу на диске, который читается или записывается, или к запросу базы данных. Неочевидная зависимость здесь - «calculate_avg». Поскольку мы тестируем только наш «модуль», мы также должны учитывать вызовы других функций как зависимости

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

  • Они могут быть отключены при автоматическом выполнении этого теста.
  • Взаимодействие с этой зависимостью может вызвать нежелательные изменения во всей системе (подумайте об изменении данных в базе данных).
  • Вы хотите протестировать различные сценарии, и вам может быть сложно заставить эту зависимость «терпеть неудачу» или выполнить тестирование с необычными данными.
  • Calcul_avg может изменить свою реализацию (возможно, сейчас она сломана), и это может привести к сбою нашего теста.
  • Потому что, если бы вы тестировали интеграцию между двумя компонентами, это был бы интеграционный тест, а не модульный тест.

Как издеваться?

У Python есть мощная библиотека имитации, которая помогает имитировать все виды зависимостей: mock. Я буду использовать декораторы во всех своих примерах, потому что в таких случаях мне легче читать тесты. Мокинг - это тот тип вещей, который я считаю лучшим обучением на примере, поэтому я составил шпаргалку с типичными имитационными сценариями. Не забудьте сослаться на нашу функцию подопытных кроликов

Исправление функции зависимости

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

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

@mock.patch("guinea_pig_module.requests.post")
@mock.patch("guinea_pig_module.requests.get")
@mock.patch("guinea_pig_module.calculate_avg")
def test_get_sum(self, mocked_avg, mocked_get, mocked_post):
    pass

Непонятные записи

Другая распространенная ошибка - имитировать правильную функцию в неправильном месте. Обратите внимание на функцию «получить» внутри «запросов». Правильный способ перезаписать «get» - передать путь guinea_pig, а затем передать «requests.get», как если бы он был объявлен внутри guinea_pig. Это не сработает:

@mock.patch("requests.get")

Удаление стандартного кода

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

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

Установка ограждений

Когда вы имитируете функцию, по умолчанию она возвращает объект Mock (). Этот объект позволяет делать что угодно. Это будет работать:

x = mocked_method()
x.fake_method()
x.not_real_method()
x.fooooo()
print(x.anything)

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

#this returns a mock object, but only with pre-existent methods
#and attributes from the class being specced
@mock.patch("your_class_here", autospec=True)

Попытка получить доступ к несуществующим элементам в исходном классе вызовет исключение.

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

@mock.patch.object("your_class_here", "target_method_here")

Побочные эффекты, вызывающие побочные эффекты

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

x.side_effect = RuntimeError("Bad")
#this will raise an exception
x()
#this will raise an exception in the 3rd call
x.side_effect = [1, 2, RuntimeError("Bad")]

Охват важен

Следующий шаг - сосредоточиться на освещении. Стремитесь к 100% охвату. Для этого вам может потребоваться написать несколько тестов для одной и той же функции. Вы будете использовать свои навыки моделирования, чтобы убедиться, что выполняются все блоки if / else и обработчиков исключений. Возвращаясь к нашей функции морской свинки, нам нужно несколько вещей, чтобы достичь 100%:

  • Вернуть запись с возрастом меньше 0
  • resp.json () должен возвращать непустой ответ. Вам нужен цикл для выполнения
  • Выбросить исключение

Очень полезный пакет, который поможет вам определить строки, в которых все еще отсутствуют тесты, - это «покрытие». Чтобы использовать это, вам нужно немного изменить предыдущую команду модульного тестирования:

python -m unittest discover
#becomes
coverage run -m unittest discover
#to see the reports from the run
coverage report -m

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

Name                      Stmts   Miss  Cover   Missing
-------------------------------------------------------
module1.py                   20      4    80%   33-35, 39
-------------------------------------------------------
TOTAL                        20      4    80%

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

Всего лишь за счет 100% покрытия эти тесты выявили 2 ошибки в коде.

  • Исключение тайм-аута запроса не было перехвачено предложением Try. Это приводило к сбою функции
  • Вторая - распространенная ошибка при создании больших блоков Try. Если «resp.json ()» вызывает исключение, «idx» никогда не инициализируется. Это приводит к возникновению исключения при печати сообщения об ошибке из предыдущего исключения. Нет, Буэно!

Как только эти ошибки будут исправлены, новая функция будет выглядеть так:

Входы и выходы

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

  • Списки: пустой список, 1 элемент, несколько элементов, огромный список
  • Файлы: пустой файл, нет файла, маленький файл, огромный файл
  • Ints и Floats: отрицательные ?, ноль, несколько положительных чисел, большие числа
  • Строки: пустая строка, маленькая строка, большая строка

Вам нужно подумать о том, что имеет смысл для функции, которую вы тестируете. Для функции «морская свинка» входные данные передавались имитируемым зависимостям. Так что не имело значения, каковы были ценности. Если вы не уверены, просто следуйте обычному списку подозреваемых. Не зацикливайтесь на этом, вам понадобится больше времени, чтобы подумать, чем писать эти тесты. Ошибитесь в сторону «больше тестов лучше»

Заключение

Помните, что модульные тесты похожи на домашнюю сигнализацию. Это необходимо для вашего спокойствия, это не должно быть отдельным проектом. Вы просто гарантируете, что изменение, внесенное через 6 месяцев, не нарушит ваше функциональное поведение. Так что потратьте больше времени на то, чтобы охватить несколько широких областей, чем очень конкретные случаи. Воспользуйтесь «спецификациями» в макете пакета. Вы только что написали свою функцию и знаете, как ее использовать. Установите эти будильники и делайте то, что вам нравится лучше всего. Стройте вещи!