Книга «Путь Python. Черный пояс по разработке, масштабированию, тестированию и развертыванию»

image Привет, Хаброжители! «Путь Python» позволяет отточить ваши профессиональные навыки и узнать как можно больше о возможностях самого популярного языка программирования. Вы научитесь писать эффективный код, создавать лучшие программы за минимальное время и избегать распространенных ошибок. Пора познакомиться с многопоточными вычислениями и мемоизацией, получить советы экспертов в области дизайна API и баз данных, а также заглянуть внутрь Python, чтобы расширит понимание языка. Вам предстоит начать проект, поработать с версиями, организовать автоматическое тестирование и выбрать стиль программирования для конкретной задачи. Потом вы перейдете к изучению эффективного объявления функции, выбору подходящих структур данных и библиотек, созданию безотказных программ, пакетам и оптимизации программ на уровне байт-кода.

Отрывок. Параллельный запуск тестов


Запуск тестовых наборов может отнимать много времени. Это частое явление в крупных проектах, когда выполнение набора тестов занимает минуты. По умолчанию pytest запускает тесты последовательно, в определенном порядке.

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

Для этого в pytest есть плагин pytest-xdist, который можно установить с помощью pip. Этот плагин расширяет командную строку pytest аргументом ––numprocesses (сокращенно –n), принимающим в качестве аргумента количество используемых ядер. Запуск pytest –n 4 запустит тестовый набор в четырех параллельных процессах, сохраняя баланс между загруженностью доступных ядер.

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

Создание объектов, используемых в тестах, с помощью фикстур


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

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

Вот пример простой фикстуры:

import pytest

@pytest.fixture
def database():
    return 

def test_insert(database):
    database.insert(123)


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

Еще одна распространенная особенность тестирования кода — это возможность удалять лишнее после работы фикстуры. Например, закрыть соединение с базой данных. Реализация фикстуры в качестве генератора добавит функциональность по очистке проверенных объектов (листинг 6.5).

Листинг 6.5. Очистка проверенного объекта

import pytest

@pytest.fixture
def database():
    db = 
    yield db
    db.close()

def test_insert(database):
    database.insert(123)

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

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

import pytest

@pytest.fixture(scope="module")
def database():
    db = 
    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"Запуск тестовых сценариев
При модульном тестировании может понадобиться запустить один и тот же тест, но с разными объектами, которые вызвали ошибку, или же прогнать весь набор тестов на другой системе.

Мы очень сильно полагались на этот метод при разработке Gnocchi, базы данных временных рядов. Gnocchi обеспечивает абстрактный класс под названием storage API. Любой класс в Python может реализовать эту абстрактную базу и зарегистрировать себя как драйвер. ПО при необходимости загружает отконфигурированный драйвер хранилища, а затем использует реализованное хранилище API для извлечения данных. В этом примере нам нужен класс модульного тестирования, который запускается с каждым драйвером (запускается для каждой реализации storage API), чтобы убедиться, что все драйверы совершают ожидаемые пользователем действия.

Этого легко добиться, если применить параметрические фикстуры, запускающие несколько раз все тесты, где они используются единожды для каждого указанного параметра. Листинг 6.6 содержит пример использования параметрических фикстур для запуска одного теста дважды, но с разными параметрами: один раз для mysql, а второй — для postgresql.

Листинг 6.6. Запуск теста с помощью параметрических фикстур

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.7. Обращение к атрибуту mock.Mock

>>> from unittest import mock
>>> m = mock.Mock()
>>> m.some_attribute = "hello world"
>>> m.some_attribute
"hello world"

Можно также динамически создавать метод для изменяемого объекта, как в листинге 6.8, где создается метод-пустышка, который всегда возвращает значение 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

Всего пара строк, и объект mock.Mock теперь имеет метод some_method (), который возвращает значение 42. Он принимает любой тип аргумента, пока проверка того, что это за аргумент, отсутствует.

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

Листинг 6.9 создает фиктивный метод, у которого есть побочный эффект — он выводит строку «hello world».

Листинг 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

Мы присвоили целую функцию атрибуту some_method ❶. Технически это позволяет реализовать более сложный сценарий в тесте, благодаря тому что можно включить любой необходимый для теста код в объект-пустышку. Далее нужно передать этот объект в функцию, которая его ожидает.

Атрибут ❷ call_count — это простой способ проверки количества раз, когда метод был вызван.

Библиотека mock использует паттерн «действие — проверка»: это значит, что после тестирования нужно убедиться, что действия, замененные на пустышки, были выполнены корректно. В листинге 6.10 применяется метод assert () к объектам-пустышкам для осуществления этих проверок.

Листинг 6.10. Вызов методов проверки

  >>> from unittest import mock
  >>> m = mock.Mock()
❶ >>> m.some_method('foo', 'bar')
  
❷ >>> 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 "", line 1, in 
    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')

Мы создали методы с аргументами foo и bar в качестве тестов, вызвав метод ❶. Простой способ проверить вызовы к объектам-пустышкам — использовать методы assert_called (), такие как assert_called_once_with () ❷. Для этих методов необходимо передать значения, которые, как вы ожидаете, будут использованы при вызове метода-пустышки. Если переданные значения отличаются от используемых, то mock вызывает исключение AssertionError. Если вы не знаете, какие аргументы могут быть переданы, используйте mock.ANY в качестве значения ❸; он заменит любой аргумент, передаваемый в метод-пустышку.

Библиотека mock также может быть использована для замены функции, метода или объекта из внешнего модуля. В листинге 6.11 мы заменили функцию os.unlink () собственной функцией-пустышкой.

Листинг 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 "", line 2, in 
  File "", line 2, in fake_os_unlink
IOError: Testing!

При использовании в качестве менеджера контекста mock.patch () заменяет целевую функцию на ту, которую мы выбираем. Это нужно, чтобы код, выполняемый внутри контекста, использовал исправленный метод. С методом mock.patch ()можно изменить любую часть внешнего кода, заставив его вести себя так, чтобы протестировать все условия для приложения (листинг 6.12).

Листинг 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).

Листинг 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 ===

Опция --cov включает вывод отчета coverage в конце тестирования. Необходимо передать имя пакета в качестве аргумента, чтобы плагин должным образом отфильтровал отчет. Вывод будет содержать строки кода, которые не были выполнены, а значит, не тестировались. Все, что вам останется, — открыть редактор и написать тест для этого кода.

Модуль coverage еще лучше — он позволяет генерировать понятные отчеты в формате HTML. Просто добавьте -–cov-report-html, и в директории htmlcov, откуда вы запустите команду, появятся HTML-страницы. Каждая страница покажет, какие части исходного кода были или не были запущены.

Если вы хотите пойти еще дальше, то используйте –-cover-fail-under-COVER_MIN_PERCENTAGE, которая приведет к сбою тестового набора, если он не покрывает минимальный процент кода. Хотя большой процент покрытия — это хорошая цель, а инструменты тестирования полезны для получения информации о состоянии тестового покрытия, сама по себе величина процента не особо информативна. Рисунок 6.1 показывает пример отчета coverage с указанием процента покрытия.

Например, покрытие кода тестами на 100% — достойная цель, но это не обязательно означает, что код тестируется полностью. Эта величина лишь показывает, что все строки кода в программе выполнены, но не сообщает, что были протестированы все условия.

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

image

Об авторе


Джульен Данжу занимается хакингом бесплатного ПО около двадцати лет, а программы на 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 высылается электронная книга.

© Habrahabr.ru