![image](https://habrastorage.org/webt/kq/of/aw/kqofawsaidazmnefdopvktmtxei.jpeg)
Отрывок. Параллельный запуск тестов
Запуск тестовых наборов может отнимать много времени. Это частое явление в крупных проектах, когда выполнение набора тестов занимает минуты. По умолчанию pytest запускает тесты последовательно, в определенном порядке.
Так как большинство компьютеров имеют многоядерные процессоры, можно ускориться, если произвести разделение тестов для запуска на нескольких ядрах.
Для этого в pytest есть плагин pytest-xdist, который можно установить с помощью pip. Этот плагин расширяет командную строку pytest аргументом ––numprocesses (сокращенно –n), принимающим в качестве аргумента количество используемых ядер. Запуск pytest –n 4 запустит тестовый набор в четырех параллельных процессах, сохраняя баланс между загруженностью доступных ядер.
Из-за того что количество ядер может различаться, плагин также принимает ключевое слово auto в качестве значения. В этом случае количество доступных ядер будет возвращено автоматически.
Создание объектов, используемых в тестах, с помощью фикстур
В модульном тестировании часто придется выполнять набор стандартных операций до и после запуска теста, и эти инструкции задействуют определенные компоненты. Например, может понадобиться объект, который будет выражать состояние конфигурации приложения, и он должен инициализироваться перед каждым тестированием, а потом сбрасываться до начальных значений после выполнения. Аналогично, если тест зависит от временного файла, этот файл должен создаваться перед тестом и удаляться после. Такие компоненты называются фикстурами. Они устанавливаются перед тестированием и пропадают после его выполнения.
В pytest фикстуры объявляются как простые функции. Функция фикстуры должна возвращать желаемый объект, чтобы в тестировании, где она используется, мог использоваться этот объект.
Вот пример простой фикстуры:
import pytest
@pytest.fixture
def database():
return <some database connection>
def test_insert(database):
database.insert(123)
Фикстура базы данных автоматически используется любым тестом, который имеет аргумент database в своем списке. Функция test_insert() получит результат функции database() в качестве первого аргумента и будет использовать этот результат по своему усмотрению. При таком использовании фикстуры не нужно повторять код инициализации базы данных несколько раз.
Еще одна распространенная особенность тестирования кода — это возможность удалять лишнее после работы фикстуры. Например, закрыть соединение с базой данных. Реализация фикстуры в качестве генератора добавит функциональность по очистке проверенных объектов (листинг 6.5).
Так как мы использовали ключевое слово yield и сделали из database генератор, то код после утверждения yield выполнится только в конце теста. Этот код закроет соединение с базой данных по завершении теста.Листинг 6.5. Очистка проверенного объекта
import pytest @pytest.fixture def database(): db = <some database connection> yield db db.close() def test_insert(database): database.insert(123)
Закрытие соединения с базой данных для каждого теста может вызвать неоправданные траты вычислительных мощностей, так как другие тесты могут использовать уже открытое соединение. В этом случае можно передать аргумент scope в декоратор фикстуры, указывая область ее видимости:
import pytest
@pytest.fixture(scope="module")
def database():
db = <some database connection>
yield db
db.close()
def test_insert(database):
database.insert(123)
Указав параметр scope = «module», вы инициализировали фикстуру единожды для всего модуля, и теперь открытое соединение с базой данных будет доступно для всех тестовых функций, запрашивающих его.
Можно запустить какой-нибудь общий код до или после теста, определив фикстуры как автоматически используемые с помощью ключевого слова autouse, а не указывать их в качестве аргумента для каждой тестовой функции. Конкретизация функции pytest.fixture() с помощью аргумента True, ключевого слова autouse, гарантирует, что фикстура вызывается каждый раз перед запуском теста в том модуле или классе, где она объявлена.
import os
import pytest
@pytest.fixture(autouse=True)
def change_user_env():
curuser = os.environ.get("USER")
os.environ["USER"] = "foobar"
yield
os.environ["USER"] = curuser
def test_user():
assert os.getenv("USER") == "foobar"</source
Такие автоматически включаемые функции удобны. Однако убедитесь, что не злоупотребляете фикстурами: они запускаются перед каждым тестом, который попадает в их область видимости, и поэтому могут затормозить работу группы тестов.
<h3>Запуск тестовых сценариев</h3>
При модульном тестировании может понадобиться запустить один и тот же тест, но с разными объектами, которые вызвали ошибку, или же прогнать весь набор тестов на другой системе.
Мы очень сильно полагались на этот метод при разработке Gnocchi, базы данных временных рядов. Gnocchi обеспечивает абстрактный класс под названием <i>storage API</i>. Любой класс в Python может реализовать эту абстрактную базу и зарегистрировать себя как драйвер. ПО при необходимости загружает отконфигурированный драйвер хранилища, а затем использует реализованное хранилище API для извлечения данных. В этом примере нам нужен класс модульного тестирования, который запускается с каждым драйвером (запускается для каждой реализации storage API), чтобы убедиться, что все драйверы совершают ожидаемые пользователем действия.
Этого легко добиться, если применить <i>параметрические фикстуры</i>, запускающие несколько раз все тесты, где они используются единожды для каждого указанного параметра. Листинг 6.6 содержит пример использования параметрических фикстур для запуска одного теста дважды, но с разными параметрами: один раз для mysql, а второй — для postgresql.
<blockquote><h4>Листинг 6.6. Запуск теста с помощью параметрических фикстур</h4>
<source lang="python">import pytest
import myapp
@pytest.fixture(params=["mysql", "postgresql"])
def database(request):
d = myapp.driver(request.param)
d.start()
yield d
d.stop()
def test_insert(database):
database.insert("somedata")
Фикстура driver получает в качестве параметра два разных значения — имена драйверов баз данных, которые поддерживаются приложением. test_insert запускается дважды: один раз для базы данных MySQL, а второй — для базы данных PostgreSQL. Это облегчает повторное прохождение одного и того же тестирования, но с разными сценариями, без добавления новых строк кода.Управляемые тесты с объектами-пустышками
Объекты-пустышки (или заглушки, mock objects) — это объекты, которые имитируют поведение реальных объектов приложения, но в особенном, управляемом состоянии. Они наиболее полезны в создании окружений, которые досконально описывают условия проведения теста. Вы можете заменить все объекты, кроме тестируемого, на объекты-пустышки и изолировать его, а также создать окружение для тестирования кода.
Один из случаев их использования — создание HTTP-клиента. Практически невозможно (или точнее, невероятно сложно) создать HTTP-сервер, на котором можно прогнать все варианты ситуаций и сценарии для каждого возможного значения. HTTP-клиенты особенно сложно тестировать на сценарии ошибок.
В стандартной библиотеке есть команда mock для создания объекта-пустышки. Начиная с Python 3.3 mock объединен с библиотекой unittest.mock. Поэтому можно использовать фрагмент кода, приведенный ниже, для обеспечения обратной совместимости между Python 3.3 и более ранними версиями:
try:
from unittest import mock
except ImportError:
import mock
Библиотека mock очень проста в применении. Любой атрибут, доступный для объекта mock.Mock, создается динамически во время выполнения программы. Такому атрибуту может быть присвоено любое значение. В листинге 6.7 mock используется для создания объекта-пустышки для атрибута-пустышки.
Можно также динамически создавать метод для изменяемого объекта, как в листинге 6.8, где создается метод-пустышка, который всегда возвращает значение 42 и принимает в качестве аргумента все что угодно.Листинг 6.7. Обращение к атрибуту mock.Mock
>>> from unittest import mock >>> m = mock.Mock() >>> m.some_attribute = "hello world" >>> m.some_attribute "hello world"
Всего пара строк, и объект mock.Mock теперь имеет метод some_method(), который возвращает значение 42. Он принимает любой тип аргумента, пока проверка того, что это за аргумент, отсутствует.Листинг 6.8. Создание метода для объекта-пустышки mock.Mock
>>> from unittest import mock >>> m = mock.Mock() >>> m.some_method.return_value = 42 >>> m.some_method() 42 >>> m.some_method("with", "arguments") 42
Динамически создаваемые методы могут также иметь (намеренные) побочные эффекты. Чтобы не быть просто шаблонными методами, которые возвращают значение, они могут быть определены для выполнения полезного кода.
Листинг 6.9 создает фиктивный метод, у которого есть побочный эффект — он выводит строку «hello world».
Мы присвоили целую функцию атрибуту some_method ❶. Технически это позволяет реализовать более сложный сценарий в тесте, благодаря тому что можно включить любой необходимый для теста код в объект-пустышку. Далее нужно передать этот объект в функцию, которая его ожидает.Листинг 6.9. Создание метода для объекта mock.Mock с побочным эффектом
>>> from unittest import mock >>> m = mock.Mock() >>> def print_hello(): ... print("hello world!") ... return 43 ... ❶ >>> m.some_method.side_effect = print_hello >>> m.some_method() hello world! 43 ❷ >>> m.some_method.call_count 1
Атрибут ❷ call_count — это простой способ проверки количества раз, когда метод был вызван.
Библиотека mock использует паттерн «действие — проверка»: это значит, что после тестирования нужно убедиться, что действия, замененные на пустышки, были выполнены корректно. В листинге 6.10 применяется метод assert() к объектам-пустышкам для осуществления этих проверок.
Мы создали методы с аргументами foo и bar в качестве тестов, вызвав метод ❶. Простой способ проверить вызовы к объектам-пустышкам — использовать методы assert_called(), такие как assert_called_once_with() ❷. Для этих методов необходимо передать значения, которые, как вы ожидаете, будут использованы при вызове метода-пустышки. Если переданные значения отличаются от используемых, то mock вызывает исключение AssertionError. Если вы не знаете, какие аргументы могут быть переданы, используйте mock.ANY в качестве значения ❸; он заменит любой аргумент, передаваемый в метод-пустышку.Листинг 6.10. Вызов методов проверки
>>> from unittest import mock >>> m = mock.Mock() ❶ >>> m.some_method('foo', 'bar') <Mock name='mock.some_method()' id='26144272'> ❷ >>> m.some_method.assert_called_once_with('foo', 'bar') >>> m.some_method.assert_called_once_with('foo', ❸mock.ANY) >>> m.some_method.assert_called_once_with('foo', 'baz') Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/usr/lib/python2.7/dist-packages/mock.py", line 846, in assert_cal led_once_with return self.assert_called_with(*args, **kwargs) File "/usr/lib/python2.7/dist-packages/mock.py", line 835, in assert_cal led_with raise AssertionError(msg) AssertionError: Expected call: some_method('foo', 'baz') Actual call: some_method('foo', 'bar')
Библиотека mock также может быть использована для замены функции, метода или объекта из внешнего модуля. В листинге 6.11 мы заменили функцию os.unlink() собственной функцией-пустышкой.
При использовании в качестве менеджера контекста mock.patch() заменяет целевую функцию на ту, которую мы выбираем. Это нужно, чтобы код, выполняемый внутри контекста, использовал исправленный метод. С методом mock.patch()можно изменить любую часть внешнего кода, заставив его вести себя так, чтобы протестировать все условия для приложения (листинг 6.12).Листинг 6.11. Использование mock.patch
>>> from unittest import mock >>> import os >>> def fake_os_unlink(path): ... raise IOError("Testing!") ... >>> with mock.patch('os.unlink', fake_os_unlink): ... os.unlink('foobar') ... Traceback (most recent call last): File "<stdin>", line 2, in <module> File "<stdin>", line 2, in fake_os_unlink IOError: Testing!
Листинг 6.12. Использование mock.patch() для тестирования множества поведений
from unittest import mock import pytest import requests class WhereIsPythonError(Exception): pass ❶ def is_python_still_a_programming_language(): try: r = requests.get("http://python.org") except IOError: pass else: if r.status_code == 200: return 'Python is a programming language' in r.content raise WhereIsPythonError("Something bad happened") def get_fake_get(status_code, content): m = mock.Mock() m.status_code = status_code m.content = content def fake_get(url): return m return fake_get def raise_get(url): raise IOError("Unable to fetch url %s" % url) ❷ @mock.patch('requests.get', get_fake_get( 200, 'Python is a programming language for sure')) def test_python_is(): assert is_python_still_a_programming_language() is True @mock.patch('requests.get', get_fake_get( 200, 'Python is no more a programming language')) def test_python_is_not(): assert is_python_still_a_programming_language() is False @mock.patch('requests.get', get_fake_get(404, 'Whatever')) def test_bad_status_code(): with pytest.raises(WhereIsPythonError): is_python_still_a_programming_language() @mock.patch('requests.get', raise_get) def test_ioerror(): with pytest.raises(WhereIsPythonError): is_python_still_a_programming_language()
Листинг 6.12 реализует тестовый случай, который ищет все экземпляры строки Python is a programming language на сайте python.org ❶. Не существует варианта, при котором тест не найдет ни одной заданной строки на выбранной веб-странице. Чтобы получить отрицательный результат, необходимо изменить страницу, а этого сделать нельзя. Но с помощью mock можно пойти на хитрость и изменить поведение запроса так, чтобы он возвращал ответ-пустышку с выдуманной страницей, не содержащей заданной строки. Это позволит протестировать отрицательный сценарий, в котором python.org не содержит заданной строки, и убедиться, что программа обрабатывает такой случай корректно.
В этом примере используется версия декоратора mock.patch() ❷. Поведение объекта-пустышки не меняется, и было проще показать пример в контексте тестовой функции.
Использование объекта-пустышки поможет сымитировать любую проблему: возвращение сервером ошибки 404, ошибку ввода-вывода или ошибку задержки сети. Мы можем убедиться, что код возвращает правильные значения или вызывает нужное исключение в каждом случае, что гарантирует ожидаемое поведение кода.
Выявление непротестированного кода с помощью coverage
Отличным дополнением для модульного тестирования является инструмент coverage [Покрытие кода — мера, используемая при тестировании. Показывает процент исходного кода программы, который был выполнен в процессе тестирования — ред.], который находит непротестированные части кода. Он использует инструменты анализа и отслеживания кода для выявления тех строк, которые были выполнены. В модульном тестировании он может выявить, какие части кода были задействованы многократно, а какие вообще не использовались. Создание тестов необходимо, а возможность узнать, какую часть кода вы забыли покрыть тестами, делает этот процесс приятнее.
Установите модуль coverage через pip, чтобы получить возможность использовать его через свою командную оболочку.
ПРИМЕЧАНИЕ
Команда также может называться python-coverage, если установка модуля происходит через установщик вашей ОС. Пример такого случая — ОС Debian.
Использовать coverage в автономном режиме довольно просто. Он показывает те части программы, которые никогда не запускаются и стали «мертвым грузом» — таким кодом, убрать который без изменения работоспособности программы уже не получится. Все тестовые инструменты, которые обсуждались ранее в главе, интегрированы с coverage.
При использовании pytest установите плагин pytest-cov через pip install pytest-pycov и добавьте несколько переключателей для генерации детального вывода непротестированного кода (листинг 6.13).
Опция --cov включает вывод отчета coverage в конце тестирования. Необходимо передать имя пакета в качестве аргумента, чтобы плагин должным образом отфильтровал отчет. Вывод будет содержать строки кода, которые не были выполнены, а значит, не тестировались. Все, что вам останется, — открыть редактор и написать тест для этого кода.Листинг 6.13. Использование pytest и coverage
$ pytest --cov=gnocchiclient gnocchiclient/tests/unit ---------- coverage: platform darwin, python 3.6.4-final-0 ----------- Name Stmts Miss Branch BrPart Cover --------------------------- gnocchiclient/__init__.py 0 0 0 0 100% gnocchiclient/auth.py 51 23 6 0 49% gnocchiclient/benchmark.py 175 175 36 0 0% --snip-- --------------------------- TOTAL 2040 1868 424 6 8% === passed in 5.00 seconds ===
Модуль coverage еще лучше — он позволяет генерировать понятные отчеты в формате HTML. Просто добавьте -–cov-report-html, и в директории htmlcov, откуда вы запустите команду, появятся HTML-страницы. Каждая страница покажет, какие части исходного кода были или не были запущены.
Если вы хотите пойти еще дальше, то используйте –-cover-fail-under-COVER_MIN_PERCENTAGE, которая приведет к сбою тестового набора, если он не покрывает минимальный процент кода. Хотя большой процент покрытия — это хорошая цель, а инструменты тестирования полезны для получения информации о состоянии тестового покрытия, сама по себе величина процента не особо информативна. Рисунок 6.1 показывает пример отчета coverage с указанием процента покрытия.
Например, покрытие кода тестами на 100 % — достойная цель, но это не обязательно означает, что код тестируется полностью. Эта величина лишь показывает, что все строки кода в программе выполнены, но не сообщает, что были протестированы все условия.
Стоит использовать информацию о покрытии с целью расширения набора тестов и создания их для кода, который не запускается. Это упрощает поддержку проекта и повышает общее качество кода.
![image](https://habrastorage.org/webt/d_/af/vl/d_afvlajlx_9ctds7yfu7ef_2ds.png)
Об авторе
Джульен Данжу занимается хакингом бесплатного ПО около двадцати лет, а программы на Python разрабатывает уже почти двенадцать лет. В настоящее время руководит проектной группой распределенной облачной платформы на основе OpenStack, которая владеет самой большой из существующих баз открытого кода Python, насчитывающей около двух с половиной миллионов строк кода. До разработки облачных сервисов Джульен занимался созданием менеджера окон и способствовал развитию многих проектов, например Debian и GNU Emacs.
О научном редакторе
Майк Дрисколл программирует на Python более десяти лет. Долгое время он писал о Python в блоге The Mouse vs. The Python. Автор нескольких книг по Python: Python 101, Python Interviews и ReportLab: PDF Processingwith Python. Найти Майка можно в Twitter и на GitHub: @driscollis.
» Более подробно с книгой можно ознакомиться на сайте издательства
» Оглавление
» Отрывок
Для Хаброжителей скидка 25% по купону — Python
По факту оплаты бумажной версии книги на e-mail высылается электронная книга.