Введение в модульное тестирование с Python
Тестирование — неотъемлемая часть разработки программного обеспечения. Основная цель двоякая:
- Хорошие тесты явно описывают, что делает то, что они тестируют; а также
- Хорошие тесты гарантируют, что то, что они тестируют, соответствует этому описанию с течением времени.
Например, представьте, что у вас есть функция, и вы хотите убедиться, что она работает так, как вы думаете. Что-то вроде этой функции, которая переворачивает список (из одной из наших предыдущих записей в блоге):
def reverse_list(original: List):
return original[::-1]
Хотя это простая функция, у вас может возникнуть ряд вопросов:
- Переворачивает ли список с ненулевым числом элементов?
- Переворачивает ли список с элементами разных типов?
- Это работает с пустым списком?
- Работает ли он со строками и другими последовательностями или только со списками?
- Изменяет ли он список на месте или дает вам новый список?
- Работает ли это для списков с повторяющимися записями?
Возможно, вы уже знаете ответы на все эти вопросы (или вы можете легко узнать их, просто запустив REPL и попробовав его). Но помните, цель тестов — описать код и сделать его устойчивым к изменениям.
Если кто-то появится позже и изменит функцию, ответы на некоторые из этих вопросов могут измениться. Вам нужно знать, если это произойдет, потому что изменение некоторых из них может привести к поломке всего вашего кода!
Замечу, что при написании тестов очень часто приходится думать о большем количестве вопросов. Мы будем писать тесты на все вопросы, которые у нас есть!
Написание тестов Python с помощью unittest
Давайте начнем с написания нашего первого теста. Обычно мой первый тест будет положительным, что означает «делает ли эта функция то, что я от нее ожидаю?».
from unittest import TestCase
from typing import List
def reverse_list(original: List) -> List:
return original[::-1]
class ReverseListTest(TestCase):
def test_reverses_nonempty_list(self):
original = [3, 7, 1, 10]
expected = [1, 3, 7, 10]
self.assertEqual(reverse_list(original), expected)
Мы написали наш первый тест!
Важно писать простые тесты, где это возможно. Обычно в своих тестах я определяю аргумент(ы) того, что тестирую (т. original
переменная), expected
результат, а затем я сравниваю два.
Запуск тестов с помощью unittest
По умолчанию при работе с unittest ваш файл должен называться test_*.py
. А пока назовите свой файл test_reverse.py
Например.
Именование тестовых методов также должно следовать этому шаблону именования по умолчанию. Обратите внимание, что мой тестовый метод называется test_reverses_nonempty_list
.
После того, как вы сохранили файл, вы можете запустить его одним из следующих способов:
- Если вы используете PyCharm, щелкните правой кнопкой мыши и нажмите «Выполнить тесты с помощью unittest…».
- Если вы используете терминал, введите
python -m unittest test_reverse.py
.
python -m unittest test_reverse.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
.
Вы видите, что ваш тест успешен!
Ответы на вопросы с тестами
Давайте ответим на наши другие вопросы еще несколькими тестами:
- Переворачивает ли список с элементами разных типов?
- Это работает с пустым списком?
- Работает ли он со строками и другими последовательностями или только со списками?
- Изменяет ли он список на месте или дает вам новый список?
- Работает ли это для списков с повторяющимися записями?
Переворачивает ли список с элементами разных типов?
Ответ на этот вопрос может показаться неактуальным. Вы можете подумать: «Я никогда этого не сделаю, так какое мне дело?».
Если ты никогда собирается сделать это, может быть, вы хотите, чтобы не сделать это случайно. Должна ли наша функция проверять, что все элементы одного типа? Вам решать, что должно происходить в вашей кодовой базе. Для целей этого примера давайте сделаем так, чтобы у нас не было смешанных типов.
from unittest import TestCase
from typing import List
def has_mixed_types(list_: List):
first = type(list_[0])
return any(not isinstance(t, first) for t in list_)
def reverse_list(original: List) -> List:
if has_mixed_types(original):
raise ValueError("The list to be reversed should have homogeneous types.")
return original[::-1]
class ReverseListTest(TestCase):
def test_reverses_nonempty_list(self):
original = [3, 7, 1, 10]
expected = [10, 1, 7, 3]
self.assertEqual(reverse_list(original), expected)
def test_reverses_varying_elements_raises(self):
original = [3, 7, "a"]
with self.assertRaises(ValueError):
reverse_list(original)
Обратите внимание, что вам не нужен явный тест на «не возникает ошибка, когда все элементы одного типа», потому что это выводится после того, как наш первый тест пройден.
Это работает с пустым списком?
Это еще один вопрос, который вы можете решить, поддерживать или нет.
Если вы предпочитаете уведомлять пользователей, когда они пытаются реверсировать пустой список, вы можете вызвать ошибку, если длина исходного списка равна 0. На данный момент мы разрешим это.
Этот тест здесь потому, что если в будущем мы решим вызвать ошибку при передаче пустого списка, этот тест должен провалиться. Затем мы возвращались и исправляли это, радуясь тому, что наша функция работает так, как мы ожидаем.
def test_reverses_empty_list(self):
original = []
expected = []
self.assertEqual(reverse_list(original), expected)
Работает ли он со строками и другими последовательностями или только со списками?
Этот вопрос возник у меня при написании этого поста!
Реверсирование с помощью срезов будет работать со строками и другими последовательностями, а также со списками. Возможно, вы хотели бы разрешить это, но на данный момент это не разрешено.
Поскольку наша подсказка типа указывает List
в качестве аргумента должны передаваться списки, иначе разработчику будет выдано предупреждение, если он использует инструменты подсказки типов (а они должны быть!).
Если вы хотите разрешить другие последовательности, вы можете использовать Sequence
вместо List
от typing
модуль.
Изменяет ли он список на месте или дает вам новый список?
Это очень важно! Если вы вызываете функцию и не уверены, изменит ли она существующий список или даст вам новый, вы обязательно столкнетесь с проблемами.
Обращение списка с использованием срезов даст вам новый список, но если вы используете другой метод для обращения, он может измениться на месте.
Вот почему так важно написать тест: если кто-то придет позже и изменит функцию, у нас должен быть провальный тест, если новая функция изменит список на месте.
def test_reversal_gives_new_list(self):
original = [3, 5, 7]
expected = [3, 5, 7]
reverse_list(original)
self.assertEqual(original, expected)
Проверяя original
и expected
одинаковы, мы знаем, что original
был оставлен без изменений reverse_list()
.
Работает ли это для списков с повторяющимися записями?
Некоторые способы реверсирования списка могут не сработать, если в списке есть повторяющиеся записи, поэтому для этого стоит добавить еще один тест — просто убедитесь, что реверсирование дает ожидаемое значение, даже если есть повторяющиеся элементы.
def test_reversal_with_duplicates(self):
original = [3, 4, 3, 3, 9]
expected = [9, 3, 3, 4, 3]
self.assertEqual(reverse_list(original), expected)
Больше вопросов
Когда вы пишете код и используете reverse_list
вы можете столкнуться с дополнительными вопросами, на которые хотели бы получить ответы.
Может быть, вы напишете об ошибке, связанной с этой функцией, или, может быть, вам просто придет в голову кое-что, в чем вы хотели бы убедиться.
Пишите больше тестов! Тесты похожи на живую документацию, и они помогают вам быть уверенными в том, что ваш код работает именно так, как вы думаете, даже по мере того, как код развивается и изменяется.
Подведение итогов
Окончательный полный код можно увидеть здесь.
Спасибо за чтение. Надеюсь, вы узнали что-то новое из этого поста!
Есть вопросы? Твитнуть у нас. Рассмотрите возможность присоединиться к нашему Полный курс Python чтобы узнать больше о Python, или наш Автоматизированное тестирование программного обеспечения с помощью Python курс для более глубокого изучения Python и веб-приложений.