Зарегистрируйтесь для доступа к 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}'

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

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

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

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

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

Дальше мы смотрим на пограничные случаи — ситуации, в которых код может вести себя по-особенному:

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

Для функции 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}'

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

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

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

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

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

# Возвращает элемент по указанному индексу
# Если индекса не существует, возвращается значение по умолчанию
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. Попробуйте их сломать
  4. Добавьте код на GitHub

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»