Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Зачем и как писать тесты? Python: Автоматическое тестирование

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

Представьте, что вы написали функцию capitalize(text), которая делает заглавной первую букву переданной строки:

capitalize('hello') # 'Hello'

Вот один из вариантов её реализации:

# main.py
def capitalize(text):
    first_char = text[0].upper()
    rest_substring = text[1:]
    return f'{first_char}{rest_substring}'

Что мы делаем после создания функции? Проверяем, как она работает. Например, открываем REPL и вызываем функцию с разными аргументами:

python -i main.py
capitalize('hello, hexlet!')
# => 'Hello, hexlet!'

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

Фактически, весь этот процесс и есть тестирование. Но не автоматическое, а ручное. Задача такого тестирования — убедиться, что код работает как надо. И нам совершенно без разницы, как конкретно реализована эта функция. Это и есть главный ответ на вопрос, заданный в начале урока.

Тесты проверяют, что код (или приложение) работает корректно. И не заботятся о том, как конкретно написан код, который они проверяют.

Автоматические тесты

Всё, что требуется от автоматических тестов — повторить проверки, которые мы выполняли, делая ручное тестирование. Для этого достаточно старого доброго if и исключений.

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

# Дословно: выбросить новое исключение. Исключения бросают.
# Код, следующий за этим выражением, не выполнится, а сам скрипт завершится с ошибкой
raise Exception('Бум! Произошла ошибка, останавливаемся')
print('nothing'); # никогда не выполнится

# После запуска получим такой вывод:
# Traceback (most recent call last):
#   File "main.py", line 8, in <module>
#     raise Exception('Бум! Произошла ошибка, останавливаемся');
# Exception: Бум! Произошла ошибка, останавливаемся

Пример теста:

# Если результат функции не равен ожидаемому значению
if capitalize('hello') != 'Hello':
    # Выбрасываем исключение и завершаем выполнение теста
    raise Exception('Функция работает неверно!')

Из примера выше видно, что тесты — это точно такой же код, как и любой другой. Он работает в том же окружении и подчиняется тем же правилам, например, стандартам кодирования. А ещё он может содержать ошибки. Но это не значит, что надо писать тесты на тесты. Избежать всех ошибок невозможно, да и не нужно, иначе стоимость разработки стала бы неоправданно высокой. Обнаруженные ошибки в тестах исправляются, и жизнь продолжается дальше ;)

В коде тесты, как правило, складывают в специальную директорию в корне проекта. Обычно она называется tests, хотя встречаются и другие варианты:

package-name/
└── __init__.py
tests/
└── test_something.py

Структура этой директории зависит от того, на базе чего пишутся тесты, например, на базе какого фреймворка. В простых случаях, она отражает структуру исходного кода. Если предположить, что наша функция capitalize(text) определена в файле package-name/capitalize.py, то её тест лучше поместить в файл tests/test_capitalize.py.

Теперь при любых изменениях, затрагивающих эту функцию, важно не забывать запускать тесты:

python tests/test_capitalize.py
# Если все хорошо, код молча выполнится
# Если есть ошибка, то будет выведено сообщение об ошибке

Как пишутся тесты

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

Если поменялась сигнатура функции (входные или выходные параметры, её имя), то придётся переписывать тесты. Если сигнатура осталась той же, но поменялись внутренности функции, как например здесь:

def capitalize(text):
    first_char, *rest_chars = text
    rest_substring = ''.join(rest_chars)
    return f'{first_char.upper()}{rest_substring}'

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

Сколько и какие нужно писать проверки?

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

При написании тестов нужно ориентироваться на разнообразие входных данных. У любой функции есть один или несколько основных сценариев использования. Например, в случае capitalize() — это любое слово. Достаточно написать ровно одну проверку, которая покрывает этот сценарий. Дальше нужно смотреть на "пограничные случаи". Это ситуации, в которых код может повести себя по-особенному:

  • Работа с пустой строкой
  • Обработка null
  • Деление на ноль (в большинстве языков вызывает ошибку)
  • Специфические ситуации для конкретных алгоритмов

Для capitalize() пограничным случаем будет пустая строка:

if capitalize('') != '':
  raise Exception('Функция работает неверно!')

Добавив тест на пустую строку, мы увидим, что вызов показанной в начале урока функции capitalize() завершается с ошибкой. Внутри неё идёт обращение к первому индексу строки без проверки его существования. Исправленная версия кода:

def capitalize(text):
    if text == '':
        return ''
    first_char = text[0].upper()
    rest_substring = text[1:]
    return f'{first_char}{rest_substring}'

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

  • Функция выполнилась так, что не был выполнен ни один условный блок
  • Функция выполнилась так, что был выполнен только первый условный блок
  • Функция выполнилась так, что был выполнен только второй условный блок
  • Функция выполнилась так, что были выполнены оба условных блока

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

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

# Возвращает элемент по указанному индексу. Если индекса не существует, возвращается значение по умолчанию
def get_by_index(elements, index, default):
  return elements[index] if index <= len(elements) else default
# В этом коде по ошибке поставили <= вместо <. Из-за того он иногда работает неправильно

# Работает правильно
get_by_index(['zero', 'one'], 1, 'value') # 'one'
# Работает неправильно
get_by_index(['zero', 'one'], 2, 'value') # Exception!

Проверка входных данных

Особняком стоят ошибки типов входных данных. Например, в функцию capitalize() можно передать число вместо строки. Как она должна себя вести в таком случае? Нужно ли писать такой тест?

Ещё один интересный вопрос. Нужно ли внутри capitalize() обрабатывать такие ситуации? Ответ — не нужно. Иначе код превратится в мусорку, а пользы от этого мало. Всё равно должны быть тесты, которые проверяют, что система работает в целом, а они обычно выявляют проблемы кода на более нижних уровнях

Ответственность за передачу правильных данных в функцию capitalize() лежит не на ней, а на коде, который вызывает эту функцию. И если он хорошо протестирован, то подобная ошибка либо обнаружится, либо вообще не возникнет.

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

Собирая всё вместе

В конечном итоге мы получили такую структуру директорий:

package-name/
└── capitalize.py
tests/
└── test_capitalize.py

Содержимое теста:

from capitalize import capitalize

if capitalize('hello') != 'Hello':
    raise Exception('Функция работает неверно!')

if capitalize('') != '':
    raise Exception('Функция работает неверно!')

print('Все тесты пройдены!')

Запуск:

PYTHONPATH=package-name python tests/test_capitalize.py

https://replit.com/@hexlet/python-testing-goal-capitalize

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


Самостоятельная работа

  1. Воспроизведите структуру получившуюся в конце урока
  2. Запустите тесты, убедитесь что они работают. Попробуйте их сломать
  3. Добавьте код на гитхаб

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты.

Ошибки, сложный материал, вопросы >
Нашли опечатку или неточность?

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

Что-то не получается или материал кажется сложным?

Загляните в раздел «Обсуждение»:

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

Об обучении на Хекслете

Для полного доступа к курсу нужен базовый план

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

Получить доступ
900
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.

  • 130 курсов, 2000+ часов теории
  • 900 практических заданий в браузере
  • 360 000 студентов
Даю согласие на обработку персональных данных, соглашаюсь с «Политикой конфиденциальности» и «Условиями оказания услуг»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы

С нуля до разработчика. Возвращаем деньги, если не удалось найти работу.

Иконка программы Python-разработчик
Профессия
Разработка веб-приложений на Django
1 июня 10 месяцев

Используйте Хекслет по максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

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