Изучаем Python Эрик Метиз. Crash course2 n d e d i t i o na h a n d s o n, p r o j e c t b a s e d i n t r o d u c t i o n t o p r o g r a m m i n g
Скачать 6.19 Mb.
|
225 имени и фамилии. Мы назвали этот метод test_first_last_name() . Любой метод, имя которого начинается с test_ , будет выполняться автоматически при запуске test_name_function .py . В тестовом методе вызывается тестируемая функция и сохра- няется возвращаемое значение, которое необходимо проверить. В данном примере вызывается функция get_formatted_name() с аргументами 'janis' и 'joplin' , а результат сохраняется в переменной formatted_name . В точке используется одна из самых полезных особенностей unittest : метод assert . Методы assert проверяют, что полученный результат соответствует тому результату, который вы рассчитывали получить. В данном случае известно, что функция get_formatted_name() должна вернуть полное имя с пробелами и капита- лизацией слов, поэтому переменная formatted_name должна содержать текст «Janis Joplin». Чтобы убедиться в этом, мы используем метод assertEqual() из модуля unittest и передаем ему переменную formatted_name и строку 'Janis Joplin' . Вызов self.assertEqual(formatted_name, 'Janis Joplin') означает: «Сравни значение formatted_name со строкой 'Janis Joplin' . Если они равны, как и ожидалось, — хорошо. Но если они не равны, обязательно сообщи мне!» Мы запустим этот файл напрямую, но важно заметить, что многие тестовые фрейм- ворки импортируют ваши тестовые файлы перед их выполнением. При импортиро- вании файла интерпретатор выполняет файл в процессе импортирования. Блок if в точке проверяет специальную переменную __name__ , значение которой задается при выполнении программы. Если файл выполняется как главная программа, то переменной __name__ будет присвоено значение '__main__' . В этом случае вызы- вается метод unittest.main() , который выполняет тестовый сценарий. Если файл импортируется тестовым сценарием, то переменная __name__ будет содержать значение '__main__' , и этот блок выполняться не будет. При запуске test_name_function .py будет получен следующий результат: ---------------------------------------------------------------------- Ran 1 test in 0.000s OK Точка в первой строке вывода сообщает, что один тест прошел успешно. Следую- щая строка говорит, что для выполнения одного теста Python потребовалось менее 0,001 секунды. Наконец, завершающее сообщение OK говорит о том, что прошли все модульные тесты в тестовом сценарии. Этот результат показывает, что функция get_formatted_name() успешно работает для полных имен, состоящих из имени и фамилии, если только функция не была изменена. В случае внесения изменений в get_formatted_name() тест можно запу- стить снова. И если тестовый сценарий снова пройдет, мы будем знать, что функция продолжает успешно работать с полными именами из двух компонентов. 226 Глава 11 • Тестирование Сбой теста Как выглядит сбойный тест? Попробуем изменить функцию 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() Эта версия должна работать для полных имен из трех компонентов, но тестирова- ние показывает, что она перестала работать для полных имен из двух компонентов. На этот раз файл test_name_function .py выдает следующий результат: ❶ E ====================================================================== ❷ ERROR: test_first_last_name (__main__.NamesTestCase) ---------------------------------------------------------------------- ❸ Traceback (most recent call last): File "test_name_function.py", line 8, in test_first_last_name formatted_name = get_formatted_name('janis', 'joplin') TypeError: get_formatted_name() missing 1 required positional argument: 'last' ---------------------------------------------------------------------- ❹ Ran 1 test in 0.000s ❺ FAILED (errors=1) Теперь информации гораздо больше, потому что при сбое теста разработчик должен знать, почему это произошло. Вывод начинается с одной буквы E , которая сообщает, что один модульный тест в тестовом сценарии привел к ошиб- ке. Затем мы видим, что ошибка произошла в тесте test_first_last_name() в NamesTestCase . Конкретная информация о сбойном тесте особенно важна в том случае, если тестовый сценарий состоит из нескольких модульных тестов. В точке — стандартная трассировка, из которой видно, что вызов функции get_formatted_name('janis', 'joplin') перестал работать из-за необходимого позиционного аргумента. Также из вывода следует, что был выполнен один модульный тест . Наконец, дополнительное сообщение информирует, что тестовый сценарий в целом не прошел и в ходе выполнения произошла одна ошибка при выполнении тестового сценария . Эта информация размещается в конце вывода, чтобы она была видна сразу; разработчику не придется прокручивать длинный протокол, чтобы узнать количество сбойных тестов. Тестирование функции 227 Реакция на сбойный тест Что делать, если тест не проходит? Если предположить, что проверяются пра- вильные условия, прохождение теста означает, что функция работает правильно, а сбой — что в новом коде добавилась ошибка. Итак, если тест не прошел, изменять нужно не тест, а код, который привел к сбою теста. Проанализируйте изменения, внесенные в функцию, и разберитесь, как они привели к нарушению ожидаемого поведения. В данном случае у функции 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 необязателен. Если второе имя передается функции, то полное имя будет содержать имя, второе имя и фами- лию. В противном случае полное имя состоит только из имени и фамилии. Теперь функция должна работать для обеих разновидностей имен. Чтобы узнать, работает ли функция для имен из двух компонентов, снова запустите test_name_function .py : ---------------------------------------------------------------------- Ran 1 test in 0.000s OK Теперь тестовый сценарий проходит. Такой исход идеален; он означает, что функ- ция снова работает для имен из двух компонентов и нам не придется тестировать функцию вручную. Исправить ошибку было несложно, потому что сбойный тест помог выявить новый код, нарушивший существующее поведение. 228 Глава 11 • Тестирование Добавление новых тестов Теперь мы знаем, что get_formatted_name() работает для простых имен, и можем на- писать второй тест для имен из трех компонентов. Для этого в класс NamesTestCase добавляется еще один метод: test_name_function.py class NamesTestCase(unittest.TestCase): """Тесты для 'name_function.py'.""" def test_first_last_name(self): def test_first_last_middle_name(self): """Работают ли такие имена, как 'Wolfgang Amadeus Mozart'?""" ❶ formatted_name = get_formatted_name( 'wolfgang', 'mozart', 'amadeus') self.assertEqual(formatted_name, 'Wolfgang Amadeus Mozart') if __name__ == '__main__': unittest.main() Новому методу присваивается имя test_first_last_middle_name() . Имя метода должно начинаться с test_ , чтобы этот метод выполнялся автоматически при за- пуске test_name_function .py . В остальном имя выбирается так, чтобы оно четко пока- зывало, какое именно поведение get_formatted_name() мы тестируем. В результате при сбое теста вы сразу видите, к каким именам он относится. Не нужно опасаться длинных имен методов в классах TestCase : имена должны быть содержательными, чтобы донести информацию до разработчика в случае сбоя, а поскольку Python вызывает их автоматически, вам никогда не придется вручную вводить эти имена при вызове. Чтобы протестировать функцию, мы вызываем get_formatted_name() c тремя компонентами , после чего используем assertEqual() для проверки того, что возвращенное полное имя совпадает с ожидаемым. При повторном запуске test_ name_function .py оба теста проходят успешно: ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK Отлично! Теперь мы знаем, что функция по-прежнему работает с именами из двух компонентов, как Janis Joplin , но можем быть уверены в том, что она сработает и для имен с тремя компонентами, такими как Wolfgang Amadeus Mozart Тестирование класса 229 УПРАЖНЕНИЯ 11.1. Город, страна: напишите функцию, которая получает два параметра: название страны и название города. Функция должна возвращать одну строку в формате «Город, Страна» — например, «Santiago, Chile». Сохраните функцию в модуле с именем city_functions .py Создайте файл test_cities .py для тестирования только что написанной функции (не забудьте импортировать unittest и тестируемую функцию). Напишите метод test_city_country() для проверки того, что вызов функции с такими значениями, как 'santiago' и 'chile' , дает правильную строку. Запустите test_cities .py и убедитесь в том, что тест test_city_country() проходит успешно. 11.2. Население: измените свою функцию так, чтобы у нее был третий обязательный пара- метр — население. В новой версии функция должна возвращать одну строку вида «Santiago, Chile — population 5 000 000». Снова запустите программу test_cities .py . Убедитесь в том, что тест test_city_country() на этот раз не проходит. Измените функцию так, чтобы параметр населения стал необязательным. Снова запустите test_cities .py и убедитесь в том, что тест test_city_country() снова проходит успешно. Напишите второй тест test_city_country_population() , который проверяет вызов функ- ции со значениями 'santiago' , 'chile' и 'population=5000000' . Снова запустите test_cities . py и убедитесь в том, что новый тест проходит успешно. Тестирование класса В первой части этой главы мы писали тесты для отдельной функции. Сейчас мы займемся написанием тестов для класса. Классы будут использоваться во многих ваших программах, поэтому возможность доказать, что ваши классы работают правильно, будет безусловно полезной. Если тесты для класса, над которым вы работаете, проходят успешно, вы можете быть уверены в том, что дальнейшая доработка класса не приведет к случайному нарушению его текущего поведения. Разные методы assert Класс unittest.TestCase содержит целое семейство проверочных методов assert Как упоминалось ранее, эти методы проверяют, выполняется ли условие, которое должно выполняться в определенной точке вашего кода. Если условие истинно, как и предполагалось, то ваши ожидания относительно поведения части вашей программы подтверждаются; вы можете быть уверены в отсутствии ошибок. Если же условие, которое должно быть истинным, окажется ложным, то Python выдает исключение. В табл. 11.1 перечислены шесть часто используемых методов assert . С их помощью можно проверить, что возвращаемые значения равны или не равны ожидаемым, что значения равны True или False или что значения входят или не входят в задан- ный список. Эти методы могут использоваться только в классах, наследующих от unittest.TestCase ; рассмотрим пример использования такого метода в контексте тестирования реального класса. 230 Глава 11 • Тестирование Таблица 11.1. Методы assert, предоставляемые модулем unittest Метод Использование assertEqual(a, b) Проверяет, что a == b assertNotEqual(a, b) Проверяет, что a != b assertTrue(x) Проверяет, что значение x истинно assertFalse(x) Проверяет, что значение x ложно assertIn(элемент, список) Проверяет, что элемент входит в список assertNotIn(элемент, список) Проверяет, что элемент не входит в список Класс для тестирования Тестирование класса имеет много общего с тестированием функции — значитель- ная часть работы направлена на тестирование поведения методов класса. Впрочем, существуют и различия, поэтому мы напишем отдельный класс для тестирования. Возьмем класс для управления проведением анонимных опросов: 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() Тестирование класса 231 Чтобы продемонстрировать, что класс AnonymousSurvey работает, напишем про- грамму, которая использует этот класс: language_survey.py from survey import AnonymousSurvey # Определение вопроса с созданием экземпляра AnonymousSurvey. question = "What language did you first learn to speak?" my_survey = AnonymousSurvey(question) # Вывод вопроса и сохранение ответов. my_survey.show_question() print("Enter 'q' at any time to quit.\n") while True: response = input("Language: ") if response == 'q': break my_survey.store_response(response) # Вывод результатов опроса. print("\nThank you to everyone who participated in the survey!") my_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 , в котором он находит- ся. Например, каждому пользователю будет разрешено ввести несколько ответов. Или вы напишете метод, который будет выводить только уникальные ответы и со- общать, сколько раз был дан тот или иной ответ. Или вы напишете другой класс для проведения неанонимных опросов. 232 Глава 11 • Тестирование Реализация таких изменений грозит повлиять на текущее поведение класса AnonymousSurvey . Например, может оказаться, что поддержка ввода нескольких ответов случайно повлияет на процесс обработки одиночных ответов. Чтобы гарантировать, что доработка модуля не нарушит существующее поведение, для класса нужно написать тесты. Тестирование класса AnonymousSurvey Напишем тест, проверяющий всего один аспект поведения AnonymousSurvey . Этот тест будет проверять, что один ответ на опрос сохраняется правильно. После того как метод будет сохранен, метод assertIn() проверяет, что он действительно на- ходится в списке ответов: test_survey.py import unittest from survey import AnonymousSurvey ❶ class TestAnonmyousSurvey(unittest.TestCase): """Тесты для класса AnonymousSurvey""" ❷ def test_store_single_response(self): """Проверяет, что один ответ сохранен правильно.""" question = "What language did you first learn to speak?" ❸ my_survey = AnonymousSurvey(question) my_survey.store_response('English') ❹ self.assertIn('English', my_survey.responses) if __name__ == '__main__': unittest.main() Программа начинается с импортирования модуля unittest и тестируемого класса AnonymousSurvey . Тестовый сценарий TestAnonymousSurvey , как и в предыдущих случаях, наследует от unittest.TestCase . Первый тестовый метод проверяет, что сохраненный ответ действительно попадает в список ответов опроса. Этому методу присваивается хорошее содержательное имя test_store_single_response() . Если тест не проходит, имя метода в выходных данных сбойного теста ясно пока- зывает, что проблема связана с сохранением отдельного ответа на опрос. Чтобы протестировать поведение класса, необходимо создать экземпляр класса. В точке создается экземпляр с именем my_survey для вопроса "What language did you first learn to speak?" . Один ответ ( English ) сохраняется с использова- нием метода store_response() . Затем программа убеждается в том, что ответ был сохранен правильно; для этого она проверяет, что значение English присутствует в списке my_survey.responses . При запуске программы test_survey .py тест проходит успешно: ---------------------------------------------------------------------- Ran 1 test in 0.001s OK |