Книга: Изучаем Python: программирование игр, визуализация данных, веб-приложения. 3-е изд. дополненное и переработанное
Назад: 10. Файлы и исключения
Дальше: Часть II. Проекты

11. Тестирование кода

21839.png

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

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

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

Установка pytest с помощью pip

Хотя Python содержит множество функций в стандартной библиотеке, разработчики Python также сильно зависят от пакетов сторонних разработчиков. Сторонний пакет (third-party package) — это библиотека, разработанная за пределами ядра языка Python. Некоторые популярные библиотеки сторонних разработчиков в конечном счете переходят в стандартную библиотеку и с этого момента добавляются в большинство установочных пакетов Python. Чаще всего это происходит с библиотеками, которые вряд ли сильно изменятся после того, как в них будут устранены первые ошибки. Такие библиотеки могут развиваться в том же темпе, что и весь язык.

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

Обновление pip

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

Откройте новое терминальное окно и выполните следующую команду:

$ python -m pip install --upgrade pip

❶ Requirement already satisfied: pip in /.../python3.11/site-packages (22.0.4)

--пропуск--

❷ Successfully installed pip-22.1.2

Первая часть этой команды, python -m pip, дает Python указание запустить модуль pip. Вторая часть, install --upgrade, дает pip указание обновить ранее установленный пакет. Последняя часть, pip, — это имя стороннего пакета, который должен быть обновлен. Согласно выводу, на моем компьютере текущая версия pip, 22.0.4 , была заменена последней версией на момент написания книги, 22.1.2 .

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

$ python -m pip install --upgrade имя_пакета

ПРИМЕЧАНИЕ

В операционной системе Linux инструмент pip может быть не включен в Python. Если при попытке обновить pip вы получаете ошибку, то обратитесь к инструкциям, приведенным в приложении A.

Установка pytest

Теперь, обновив версию pip, мы можем установить pytest:

$ python -m pip install --user pytest

Collecting pytest

  --пропуск--

Successfully installed attrs-21.4.0 iniconfig-1.1.1 ...pytest-7.x.x

Мы по-прежнему используем основную команду, pip install, однако на этот раз без флага --upgrade. Вместо этого мы используем флаг --user, давая Python указание установить этот пакет только для текущего пользователя. Согласно выводу, последняя версия pytest успешно установлена, как и ряд других пакетов, необходимых для работы pytest.

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

$ python -m pip install --user имя_пакета

ПРИМЕЧАНИЕ

Если у вас возникли трудности с выполнением этой команды, то попробуйте выполнить ее без флага --user.

Тестирование функции

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

name_function.py

def get_formatted_name(first, last):

    """Генерирует отформатированное полное имя."""

    full_name = f"{first} {last}"

    return full_name.title()

Функция get_formatted_name() формирует полное имя из имени и фамилии, разделив их пробелом, преобразует первый символ каждого слова в верхний регистр и возвращает полученный результат. Чтобы убедиться в том, что эта функция работает правильно, мы напишем программу, которая ее использует. Программа names.py запрашивает у пользователя имя и фамилию и выдает отформатированное полное имя:

names.py

from name_function import get_formatted_name

 

print("Enter 'q' at any time to quit.")

while True:

    first = input("\nPlease give me a first name: ")

    if first == 'q':

        break

    last = input("Please give me a last name: ")

    if last == 'q':

        break

 

    formatted_name = get_formatted_name(first, last)

    print(f"\tNeatly formatted name: {formatted_name}.")

Программа импортирует функцию get_formatted_name() из модуля name_function.py. Пользователь вводит последовательность имен и фамилий и видит, что программа сгенерировала отформатированные полные имена:

Enter 'q' at any time to quit.

 

Please give me a first name: janis

Please give me a last name: joplin

       Neatly formatted name: Janis Joplin.

 

Please give me a first name: bob

Please give me a last name: dylan

       Neatly formatted name: Bob Dylan.

 

Please give me a first name: q

Как видно из листинга, имена сгенерированы правильно. Но, допустим, вы решили изменить функцию get_formatted_name(), чтобы она также работала со вторыми именами. При этом необходимо проследить за тем, чтобы функция не перестала правильно работать для имен, состоящих только из имени и фамилии. Чтобы протестировать код, можно запустить names.py и для проверки вводить имя из двух компонентов (скажем, Janis Joplin) при каждом изменении get_formatted_name(), но это довольно утомительно. К счастью, Python предоставляет эффективный механизм автоматизации тестирования вывода функций. При автоматизации тестирования get_formatted_name() вы будете уверены в том, что функция успешно работает для всех видов имен, для которых написаны тесты.

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

Существует множество подходов к тестированию программного обеспечения. Одним из самых простых видов тестирования является модульное тестирование. Модульный тест (unit test) проверяет правильность работы одного конкретного аспекта поведения функции. Тестовый сценарий (test case) представляет собой совокупность модульных тестов, которые совместно доказывают, что функция ведет себя так, как положено, во всем диапазоне ситуаций, которые она должна обрабатывать. Хороший тестовый сценарий учитывает все возможные виды ввода, которые может получать функция, и содержит тесты для представления всех таких ситуаций. Тестовый сценарий с полным покрытием (full coverage) содержит обширный спектр модульных тестов, охватывающих все возможные варианты использования функции. Обеспечение полного покрытия может быть весьма непростой задачей в крупном проекте. Часто бывает достаточно написать модульные тесты для критичных аспектов поведения вашего кода, а затем стремиться к полному покрытию только в том случае, если проект перейдет в фазу масштабного использования.

Прохождение теста

С помощью pytest создать модульный тест достаточно просто. Мы напишем одну тестовую функцию. Она будет вызывать тестируемую функцию, а мы — утверждать возвращаемое значение. Если наше утверждение верно, то тест пройдет; в противном случае — будет провален.

Вот тестовый сценарий, который проверяет, что функция get_formatted_name() работает правильно:

test_name_function.py

from name_function import get_formatted_name

 

❶ def test_first_last_name():

    """Поддерживаются ли имена типа 'Janis Joplin'?"""

❷     formatted_name = get_formatted_name('janis', 'joplin')

❸     assert formatted_name == 'Janis Joplin'

Прежде чем запустить тест, рассмотрим эту функцию. Имя файла с тестом очень важно; оно должно начинаться со слова test_. При запуске написанных нами тестов с помощью pytest этот модуль найдет все файлы, имена которых начинаются с test_, и запустит все содержащиеся в них тесты.

В файле с тестом мы сначала импортируем функцию, которую хотим протестировать: get_formatted_name(). Затем определяем тестовую функцию: в данном случае это test_first_last_name() . Это имя функции более длинное, чем использованное ранее, и на то есть веские причины. Так, тестовые функции должны начинаться со слова test, за которым следует символ подчеркивания. Все функции, имена которых начинаются с test_, будут определены модулем pytest и запущены в процессе тестирования.

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

Далее мы вызываем тестируемую функцию , в данном случае get_formatted _name() с аргументами 'janis' и 'joplin', точно так же, как и при запуске файла names.py. Результат выполнения этой функции мы присваиваем переменной formatted_name.

Наконец, мы создаем утверждение . Так мы утверждаем, что соблюдается то или иное условие. Здесь мы утверждаем, что переменной formatted_name должно быть присвоено значение 'Janis Joplin'.

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

Запустив файл test_name_function.py вручную, вы не получите результат, поскольку мы так и не вызвали тестовую функцию. Вместо этого мы попросим pytest запустить тестовый файл.

Для этого откройте терминальное окно и перейдите в папку, содержащую файл с тестом. В редакторе VS Code вы можете открыть папку, в которой находится файл с тестом, и использовать терминал, встроенный в окно редактора. В терминальном окне введите команду pytest. Вот что вы должны увидеть:

$ pytest

========================= test session starts =========================

❶ platform darwin -- Python 3.x.x, pytest-7.x.x, pluggy-1.x.x

❷ rootdir: /.../python_work/chapter_11

❸ collected 1 item

 

❹ test_name_function.py .                                          [100%]

========================== 1 passed in 0.00s ==========================

Разберем, что мы видим в выводе. Прежде всего здесь отображена информация о системе, в которой выполняется тест . Я тестирую программу в операционной системе macOS, так что в вашем случае вывод может быть несколько иным. Самое главное — указано, какие версии Python, pytest и других пакетов используются для выполнения теста.

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

Последняя строка говорит о том, что один тест пройден и на его выполнение ушло менее 0,01 секунды.

Согласно результатам теста, функция get_formatted_name() успешно работает для полных имен, состоящих из имени и фамилии, если только функция не была изменена. В случае внесения изменений в get_formatted_name() тест можно запустить снова. И если тестовый сценарий опять пройдет, то мы будем знать, что функция продолжает успешно работать с полными именами типа «Дженис Джоплин».

ПРИМЕЧАНИЕ

Если вы не знаете, как перейти в нужный каталог в терминале, то см. раздел «Запуск программ Python из терминала» в главе 1. А если выводится сообщение о том, что команда pytest не найдена, то вместо команды pytest используйте команду python -m pytest.

Сбой теста

Что произойдет при провале теста? Попробуем изменить функцию get_formatted_name(), чтобы она работала со вторыми именами, — но сделаем это так, чтобы она перестала работать с полными данными из имени и фамилии типа «Дженис Джоплин».

Новая версия get_formatted_name() с дополнительным аргументом второго имени выглядит так:

name_function.py

def get_formatted_name(first, middle, last):

    """Генерирует отформатированное полное имя."""

    full_name = f"{first} {middle} {last}"

    return full_name.title()

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

На этот раз pytest выдает следующий результат:

$ pytest

========================= test session starts =========================

--пропуск--

❶ test_name_function.py F                                          [100%]

❷ ============================== FAILURES ===============================

❸ ________________________ test_first_last_name _________________________

    def test_first_last_name():

        """Поддерживаются ли имена типа 'Janis Joplin'?"""

❹ >       formatted_name = get_formatted_name('janis', 'joplin')

❺ E       TypeError: get_formatted_name() missing 1 required positional

           argument: 'last'

 

test_name_function.py:5: TypeError

======================= short test summary info =======================

FAILED test_name_function.py::test_first_last_name - TypeError:

    get_formatted_name() missing 1 required positional argument: 'last'

========================== 1 failed in 0.04s ==========================

На этот раз информации гораздо больше, поскольку при сбое теста разработчик должен знать, почему это произошло. Вывод начинается с одной буквы F , которая сообщает, что один модульный тест в тестовом сценарии привел к ошибке. Далее приведен раздел FAILURES , так как тесты, завершенные неудачно, обычно наиболее важны и на них следует обратить внимание при тестировании. Затем мы видим, что ошибка произошла в тесте test_first_last_name() . Угловая скобка указывает на строку кода, которая привела к сбою тестирования. Буква E в следующей строке отражает фактическую ошибку, которая привела к сбою: ошибку TypeError из-за отсутствия необходимого позиционного аргумента last. Наиболее важная информация повторяется в краткой выжимке в конце, поскольку при выполнении множества тестов программисту важно быстро понять, какие тесты провалились и почему.

Реакция на сбойный тест

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

В данном случае у функции get_formatted_name() было всего два обязательных параметра: имя и фамилия. Теперь она требует три обязательных параметра: имя, второе имя и фамилию. Добавление обязательного параметра для второго имени нарушило ожидаемое поведение get_formatted_name(). В таком случае лучше всего сделать параметр второго имени необязательным. После этого тесты для имен с двумя компонентами снова будут завершаться успешно, и программа сможет получать также вторые имена. Изменим функцию get_formatted_name(), чтобы параметр второго имени перестал быть обязательным, и снова выполним тестовый сценарий. Если он пройдет, то можно переходить к проверке правильности обработки вторых имен.

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

name_function.py

def get_formatted_name(first, last, middle=''):

    """Создает отформатированное полное имя."""

    if middle:

        full_name = f"{first} {middle} {last}"

    else:

        full_name = f"{first} {last}"

    return full_name.title()

В новой версии функции get_formatted_name() параметр middle необязателен. Если второе имя передается функции, то полное будет содержать имя, второе имя и фамилию. В противном случае полное имя состоит только из имени и фамилии. Теперь функция должна работать для обеих разновидностей имен. Чтобы узнать, работает ли функция для имен из двух компонентов типа Janis Joplin, снова запустите файл test_name_function.py:

$ pytest

========================= test session starts =========================

--пропуск--

test_name_function.py .                                        [100%]

========================== 1 passed in 0.00s ==========================

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

Добавление новых тестов

Теперь мы знаем, что get_formatted_name() работает для простых имен, и можем написать второй тест для имен из трех компонентов. Для этого в файл test_name_function.py добавим еще одну тестовую функцию:

test_name_function.py

from name_function import get_formatted_name

 

def test_first_last_name():

    --пропуск--

 

def test_first_last_middle_name():

    """Поддерживаются ли такие имена, как 'Wolfgang Amadeus Mozart'?"""

❶     formatted_name = get_formatted_name('wolfgang', 'mozart', 'amadeus')

❷     assert formatted_name == 'Wolfgang Amadeus Mozart'

Новой функции присваивается имя test_first_last_middle_name(). Имя должно начинаться со слова test_, чтобы эта функция выполнялась автоматически при запуске pytest. В остальном имя выбирается так, чтобы оно четко показывало, какое именно поведение get_formatted_name() мы тестируем. В результате при сбое теста вы сразу видите, к каким именам он относится.

Чтобы протестировать функцию, мы вызываем get_formatted_name() c тремя компонентами , после чего утверждаем , что возвращенное полное имя совпадает с ожидаемым. При повторном запуске pytest оба теста завершаются успешно:

$ pytest

========================= test session starts =========================

--пропуск--

collected 2 items

 

❶ test_name_function.py ..                                         [100%]

========================== 2 passed in 0.01s ==========================

Две точки означают, что два теста пройдены, что подтверждается в последней строке вывода. Отлично! Теперь мы знаем, что функция по-прежнему работает с именами из двух компонентов, как Janis Joplin, но можем быть уверены в том, что она сработает и для имен с тремя компонентами, таких как Wolfgang Amadeus Mozart.

Упражнения

11.1. Город, страна. Напишите функцию, которая получает два параметра: название страны и название города. Функция должна возвращать одну строку в формате «Город, Страна» — например, Santiago, Chile. Сохраните функцию в модуле city_functions.py. в новой папке, чтобы pytest не выполнял тесты, которые мы уже написали.

Создайте файл test_cities.py для тестирования только что написанной функции. Напишите функцию test_city_country(), проверяющую, дает ли вызов функции с такими значениями, как 'santiago' и 'chile', правильную строку. Запустите test_cities.py и убедитесь в том, что тест test_city_country() проходит успешно.

11.2. Население. Измените свою функцию так, чтобы у нее был третий обязательный параметр — население. В новой версии функция должна возвращать одну строку вида «Город, Страна — население ххх», например, Santiago, Chile - population 5000000. Снова запустите тестирование. Убедитесь в том, что тест test_city_country() на этот раз не проходит.

Измените функцию так, чтобы параметр населения стал необязательным. Снова запустите тестирование и убедитесь в том, что тест test_city_country() снова проходит успешно.

Напишите второй тест test_city_country_population(), который проверяет вызов функции со значениями 'santiago', 'chile' и 'population=5000000'. Снова запустите тестирование и убедитесь в том, что новый тест завершается успешно.

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

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

Разные методы утверждений

До сих пор вы видели только один вид утверждений: что строка имеет определенное значение. При написании теста вы можете сделать любое утверждение, которое может быть выражено в виде условного оператора. Если условие истинно, как и предполагалось, то ваши ожидания относительно поведения части вашей программы подтверждаются; вы можете быть уверены в отсутствии ошибок. Если же условие, которое должно быть истинным, окажется ложным, то тест не будет пройден и вы узнаете, что есть проблема, которую нужно решить. В табл. 11.1 перечислено несколько часто используемых методов assert, которые можно добавить в начальные тесты.

Таблица 11.1. Распространенные операторы утверждений в тестах

Утверждение

Использование

assert a == b

Проверяет, что два значения равны

assert a != b

Проверяет, что два значения не равны

assert a

Проверяет, что значение оценивается как истинное

assert not a

Проверяет, что значение оценивается как ложное

assert элемент in список

Проверяет, что элемент входит в список

assert элемент not in список

Проверяет, что элемент не входит в список

Это лишь несколько примеров; все, что может быть выражено как условный оператор, может быть добавлено в тест.

Класс для тестирования

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

survey.py

class AnonymousSurvey():

    """Собирает анонимные ответы на опросы."""

 

❶     def __init__(self, question):

        """Сохраняет вопрос и готовится к сохранению ответов."""

        self.question = question

        self.responses = []

 

❷     def show_question(self):

        """Выводит вопрос."""

        print(self.question)

 

❸     def store_response(self, new_response):

        """Сохраняет один ответ на опрос."""

        self.responses.append(new_response)

 

❹     def show_results(self):

        """Выводит все полученные ответы."""

        print("Survey results:")

        for response in self.responses:

             print(f"- {response}")

Класс начинается с вопроса, который вы предоставили , и содержит пустой список для хранения ответов. Класс содержит методы для вывода вопроса , добавления нового ответа в список ответов и вывода всех ответов, хранящихся в списке . Чтобы создать экземпляр на основе этого класса, необходимо предоставить вопрос. После того как будет создан экземпляр, представляющий конкретный опрос, программа выводит вопрос с помощью метода show_question(), сохраняет ответ с помощью метода store_response() и выводит результаты, используя вызов show_results().

Чтобы продемонстрировать работу класса AnonymousSurvey, напишем программу, которая использует его:

language_survey.py

from survey import AnonymousSurvey

 

# Определение вопроса с созданием экземпляра AnonymousSurvey.

question = "What language did you first learn to speak?"

language_survey = AnonymousSurvey(question)

 

# Вывод вопроса и сохранение ответов.

language_survey.show_question()

print("Enter 'q' at any time to quit.\n")

while True:

    response = input("Language: ")

    if response == 'q':

        break

    language_survey.store_response(response)

 

# Вывод результатов опроса.

print("\nThank you to everyone who participated in the survey!")

language_survey.show_results()

Программа определяет вопрос и на его основе создает объект AnonymousSurvey. Затем она вызывает метод show_question(), который позволяет вывести вопрос, после чего переходит к получению ответов. Каждый ответ сохраняется сразу же при получении. Когда ввод ответов был завершен (пользователь ввел q), метод show_results() выводит результаты опроса:

What language did you first learn to speak?

Enter 'q' at any time to quit.

 

Language: English

Language: Spanish

Language: English

Language: Mandarin

Language: q

 

Thank you to everyone who participated in the survey!

Survey results:

- English

- Spanish

- English

- Mandarin

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

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

Тестирование класса AnonymousSurvey

Напишем тест, проверяющий всего один аспект поведения AnonymousSurvey, — что один ответ на опрос сохраняется правильно. После того как метод будет сохранен, метод assertIn() проверяет, действительно ли он находится в списке ответов:

test_survey.py

from survey import AnonymousSurvey

 

❶ def test_store_single_response():

    """Проверяет, что один ответ сохранен правильно."""

    question = "What language did you first learn to speak?"

❷     language_survey = AnonymousSurvey(question)

    language_survey.store_response('English')

❸     assert 'English' in language_survey.responses

Программа начинается с импортирования тестируемого класса AnonymousSurvey. Первая тестовая функция проверяет, сохраняется ли ответ на вопрос в список ответов. Этому методу присваивается хорошее описательное имя test_store_single_response() . Если тест не проходит, то имя метода в сводке этого теста ясно показывает, что проблема связана с сохранением отдельного ответа на опрос.

Чтобы протестировать поведение класса, необходимо создать экземпляр класса. Мы создаем экземпляр language_survey для вопроса "What language did you first learn to speak?". Один ответ (English) сохраняется с помощью метода store_response(). Затем программа убеждается в том, что ответ был сохранен правильно; для этого она проверяет, что значение English присутствует в списке language_survey.responses .

По умолчанию команда pytest без аргументов запускает все тесты, найденные в текущем каталоге. Чтобы сосредоточиться на тестах в одном файле, передаем имя файла с тестами, который следует выполнить. Здесь мы выполним только один тест, созданный для AnonymousSurvey:

$ pytest test_survey.py

========================= test session starts =========================

--snip--

test_survey.py .                                                 [100%]

========================== 1 passed in 0.01s ==========================

Неплохо, но опрос с одним ответом вряд ли можно назвать полезным. Убедимся в том, что три ответа сохраняются правильно. Для этого в TestAnonymousSurvey добавляется еще один метод:

from survey import AnonymousSurvey

 

def test_store_single_response():

    --пропуск--

 

def test_store_three_responses():

    """Проверяет, что три ответа были сохранены правильно."""

    question = "What language did you first learn to speak?"

    language_survey = AnonymousSurvey(question)

❶     responses = ['English', 'Spanish', 'Mandarin']

    for response in responses:

        language_survey.store_response(response)

 

❷     for response in responses:

        assert response in language_survey.responses

Новой функции присваивается имя test_store_three_responses(). Мы создаем объект опроса по аналогии с тем, как это делалось в test_store_single_response(). Затем определяется список, содержащий три разных ответа , и для каждого из этих ответов вызывается метод store_response(). После того как ответы будут сохранены, следующий цикл проверяет, что каждый ответ теперь присутствует в language_survey.responses .

Если снова запустить test_survey.py, то оба теста (для одного ответа и для трех ответов) проходят успешно:

$ pytest test_survey.py

========================= test session starts =========================

--пропуск--

test_survey.py ..                                                [100%]

========================== 2 passed in 0.01s ==========================

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

Фикстуры

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

Выполнять тестирование помогают фикстуры (fixture). Часто это объекты, используемые несколькими тестами. Фикстуры в pytest создаются с помощью функций с декоратором @pytest.fixture. Декоратор (decorator) — это директива, размещаемая непосредственно перед определением функции; Python применяет эту директиву к функции перед ее запуском, чтобы изменить поведение функции. Не волнуйтесь, если не совсем поняли принцип работы; вы можете использовать декораторы из сторонних пакетов, пока не научитесь писать их самостоятельно.

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

import pytest

from survey import AnonymousSurvey

 

❶ @pytest.fixture

❷ def language_survey():

    """Опрос, который будет доступен для всех функций тестирования."""

    question = "What language did you first learn to speak?"

    language_survey = AnonymousSurvey(question)

    return language_survey

 

❸ def test_store_single_response(language_survey):

    """Проверяет, правильно ли хранится один ответ."""

    language_survey.store_response('English')

    assert 'English' in language_survey.responses

 

❺ def test_store_three_responses(language_survey):

    """Проверяет, правильно ли хранятся три отдельных ответа."""

    responses = ['English', 'Spanish', 'Mandarin']

    for response in responses:

        language_survey.store_response(response)

 

    for response in responses:

        assert response in language_survey.responses

Нам нужно импортировать пакет pytest, поскольку мы используем определенный в нем декоратор. Мы применяем декоратор @pytest.fixture к созданной функции language_survey() . Эта функция создает объект AnonymousSurvey и возвращает новый опрос.

Обратите внимание, что определения обеих тестовых функций изменились ❸❺; теперь у каждой тестовой функции есть параметр language_survey. Если параметр в тестовой функции совпадает с именем функции, имеющей декоратор @pytest.fixture, то эта фикстура будет запущена автоматически, а возвращаемое значение — передано тестовой функции. В этом примере функция language_survey() снабжает test_store_single_response() и test_store_three_responses() экземпляром language_survey.

В тестовых функциях нет нового кода, но обратите внимание, что из них были удалены две строки ❹❻: определяющая вопрос и создающая объект AnonymousSurvey.

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

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

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

Упражнения

11.3. Штат компании. Напишите класс Employee, представляющий работника. Метод __init__() должен получать данные об имени, фамилии и ежегодном окладе; все эти значения должны сохраняться в атрибутах. Напишите метод give_raise(), который по умолчанию увеличивает ежегодный оклад на 5000 долларов, но при этом может получать другую сумму прибавки.

Напишите тестовый сценарий для Employee. Напишите два тестовых метода: test_give_default_raise() и test_give_custom_raise(). Используйте метод setUp(), чтобы вам не приходилось заново создавать экземпляр Employee в каждом тестовом методе. Запустите свой тестовый сценарий и убедитесь в том, что оба теста прошли успешно.

Резюме

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

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

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

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

Назад: 10. Файлы и исключения
Дальше: Часть II. Проекты