Как стать автором
Обновить

Python Testing с pytest. Использование pytest с другими инструментами, ГЛАВА 7

Время на прочтение24 мин
Количество просмотров36K
Автор оригинала: Okken Brian

Вернуться


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



Примеры в этой книге написаны с использованием Python 3.6 и pytest 3.2. pytest 3.2 поддерживает Python 2.6, 2.7 и Python 3.3+.


Исходный код для проекта Tasks, а также для всех тестов, показанных в этой книге, доступен по ссылке на веб-странице книги в pragprog.com. Вам не нужно загружать исходный код, чтобы понять тестовый код; тестовый код представлен в удобной форме в примерах. Но что бы следовать вместе с задачами проекта, или адаптировать примеры тестирования для проверки своего собственного проекта (руки у вас развязаны!), вы должны перейти на веб-страницу книги и скачать работу. Там же, на веб-странице книги есть ссылка для сообщений errata и дискуссионный форум.

Под спойлером приведен список статей этой серии.



pdb: Debugging Test Failures


Модуль pdb является отладчиком Python в стандартной библиотеке. Вы используете --pdb, чтобы pytest начал сеанс отладки в точке сбоя. Давайте посмотрим на pdb в действии в контексте проекта Tasks.


В "Параметризации Фикстур" на странице 64 мы оставили проект Tasks с несколькими ошибками:


$ cd /path/to/code/ch3/c/tasks_proj
$ pytest --tb=no -q
.........................................FF.FFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF.FFF...........
42 failed, 54 passed in 4.74 seconds

Прежде чем мы рассмотрим, как pdb может помочь нам отладить этот тест, давайте взглянем на доступные параметры pytest, чтобы ускорить отладку ошибок теста, которые мы впервые рассмотрели в разделе "Использование Опций" на стр.9:


  • --tb=[auto/long/short/line/native/no]: Управляет стилем трассировки.
  • -v / --verbose: Отображает все имена тестов, пройденных или не пройденных.
  • -l / --showlocals: Отображает локальные переменные рядом с трассировкой стека.
  • -lf / --last-failed: Запускает только тесты, которые завершились неудачей.
  • -x / --exitfirst: Останавливает тестовую сессию при первом сбое.
  • --pdb: Запускает интерактивный сеанс отладки в точке сбоя.



Installing MongoDB




Как упомянуто в главе 3, "Pytest Fixtures", на странице 49, для запуска тестов MongoDB требуется установка MongoDB и pymongo.


Я тестировал версию Community Server, найденную по адресу https://www.mongodb.com/download-center. pymongo устанавливается с pip:pip install pymongo. Однако это последний пример в книге, где используется MongoDB. Чтобы опробовать отладчик без использования MongoDB, можно выполнить команды pytest из code/ch2/, так как этот каталог также содержит несколько неудачных тестов.




Мы просто запустили тесты из code/ch3/c, чтобы убедиться, что некоторые из них не работают. Мы не видели tracebacks или имен тестов, потому что --tb=no отключает трассировку, и у нас не было включено --verbose. Давайте повторим ошибки (не более трех) с подробным текстом:


$ pytest --tb=no --verbose --lf --maxfail=3
============================= test session starts =============================

collected 96 items / 52 deselected
run-last-failure: rerun previous 44 failures

tests/func/test_add.py::test_add_returns_valid_id[mongo] ERROR           [  2%]
tests/func/test_add.py::test_added_task_has_id_set[mongo] ERROR          [  4%]
tests/func/test_add.py::test_add_increases_count[mongo] ERROR            [  6%]

=================== 52 deselected, 3 error in 0.72 seconds ====================

Теперь мы знаем, какие тесты провалились. Давайте рассмотрим только один из них, используя -x, включив трассировку, не используя --tb=no, и показывая локальные переменные с -l:


$ pytest -v --lf -l -x
===================== test session starts ======================
run-last-failure: rerun last 42 failures
collected 96 items
tests/func/test_add.py::test_add_returns_valid_id[mongo] FAILED
=========================== FAILURES ===========================
_______________ test_add_returns_valid_id[mongo] _______________
tasks_db = None

    def test_add_returns_valid_id(tasks_db):
        """tasks.add(<valid task>) should return an integer."""
        # GIVEN an initialized tasks db
        # WHEN a new task is added
        # THEN returned task_id is of type int
        new_task = Task('do something')
        task_id = tasks.add(new_task)   
> assert isinstance(task_id, int)
E AssertionError: assert False
E + where False = isinstance(ObjectId('59783baf8204177f24cb1b68'), int)
new_task = Task(summary='do something', owner=None, done=False, id=None)

task_id = ObjectId('59783baf8204177f24cb1b68')
tasks_db = None
tests/func/test_add.py:16: AssertionError

!!!!!!!!!!!! Interrupted: stopping after 1 failures !!!!!!!!!!!!
===================== 54 tests deselected ======================
=========== 1 failed, 54 deselected in 2.47 seconds ============

Довольно часто этого достаточно, чтобы понять почему случился провал теста. В этом конкретном случае довольно ясно, что task_id не является целым числом—это экземпляр ObjectId. ObjectId — это тип, используемый MongoDB для идентификаторов объектов в базе данных. Мое намерение со слоем tasksdb_pymongo.py было скрыть определенные детали реализации MongoDB от остальной части системы. Понятно, что в этом случае это не сработало.


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


$ pytest -v --lf -x --pdb
===================== test session starts ======================
run-last-failure: rerun last 42 failures
collected 96 items
tests/func/test_add.py::test_add_returns_valid_id[mongo] FAILED
>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>
tasks_db = None
    def test_add_returns_valid_id(tasks_db):
        """tasks.add(<valid task>) should return an integer."""
        # GIVEN an initialized tasks db
        # WHEN a new task is added
        # THEN returned task_id is of type int
        new_task = Task('do something')
        task_id = tasks.add(new_task)
> assert isinstance(task_id, int)
E AssertionError: assert False
E + where False = isinstance(ObjectId('59783bf48204177f2a786893'), int)
tests/func/test_add.py:16: AssertionError
>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>
> /path/to/code/ch3/c/tasks_proj/tests/func/test_add.py(16)
> test_add_returns_valid_id()
-> assert isinstance(task_id, int)
(Pdb)

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


  • p/print expr: Печатает значение exp.
  • pp expr: Pretty печатает значение expr.
  • l/list: Перечисляет точку сбоя и пять строк кода выше и ниже.
  • l/list begin,end: Перечисляет конкретные номера строк.
  • a/args: Печатает аргументы текущей функции с их значениями.
  • u/up: Перемещается на один уровень вверх по трассе стека.
  • d/down: Перемещается вниз на один уровень в трассировке стека.
  • q/quit: Завершает сеанс отладки.

Другие навигационные команды, такие как step и next, не очень полезны, так как мы сидим прямо в операторе assert. Вы также можете просто ввести имена переменных и получить значения.


Можно использовать p/print expr аналогично параметру -l/--showlocals для просмотра значений в функции:


(Pdb) p new_task
Task(summary='do something', owner=None, done=False, id=None)
(Pdb) p task_id
ObjectId('59783bf48204177f2a786893')
(Pdb)

Теперь можно выйти из отладчика и продолжить тестирование.


(Pdb) q
!!!!!!!!!!!! Interrupted: stopping after 1 failures !!!!!!!!!!!!
===================== 54 tests deselected ======================
========== 1 failed, 54 deselected in 123.40 seconds ===========

Если бы мы не использовали , pytest бы снова открыл Pdb в следующем теста. Дополнительные сведения об использовании модуля pdb доступны в документации Python.


Coverage.py: Определение объема тестируемого кода


Покрытие кода является показателем того, какой процент тестируемого кода тестируется набором тестов. Когда вы запускаете тесты для проекта «Tasks», некоторые функции «Tasks» выполняются с каждым тестом, но не со всеми.


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


Coverage.py является предпочтительным инструментом покрытия Python, который измеряет покрытие кода.


Вы будете использовать его для проверки кода проекта Tasks с помощью pytest.


Что бы использовать coverage.py нужно его установить. Не помешает установить плагин под названием pytest-cov, который позволит вам вызывать coverage.py от pytest с некоторыми дополнительными опциями pytest. Поскольку coverage является одной из зависимостей pytest-cov, достаточно установить pytest-cov и он притянет за собой coverage.py:


$ pip install pytest-cov
Collecting pytest-cov
   Using cached pytest_cov-2.5.1-py2.py3-none-any.whl
Collecting coverage>=3.7.1 (from pytest-cov)
   Using cached coverage-4.4.1-cp36-cp36m-macosx_10_10_x86
...
Installing collected packages: coverage, pytest-cov
Successfully installed coverage-4.4.1 pytest-cov-2.5.1

Давайте запустим отчет о покрытии для второй версии задач. Если у вас все еще установлена первая версия проекта Tasks, удалите ее и установите версию 2:


$ pip uninstall tasks
Uninstalling tasks-0.1.0:
  /path/to/venv/bin/tasks
  /path/to/venv/lib/python3.6/site-packages/tasks.egg-link
Proceed (y/n)? y
  Successfully uninstalled tasks-0.1.0
$ cd /path/to/code/ch7/tasks_proj_v2
$ pip install -e .
Obtaining file:///path/to/code/ch7/tasks_proj_v2
...
Installing collected packages: tasks
  Running setup.py develop for tasks
Successfully installed tasks
$ pip list
...
tasks (0.1.1, /path/to/code/ch7/tasks_proj_v2/src)
...

Теперь, когда установлена следующая версия задач, можно запустить отчет о базовом покрытии:


$ cd /path/to/code/ch7/tasks_proj_v2
$ pytest --cov=src

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

plugins: mock-1.6.2, cov-2.5.1
collected 62 items
tests/func/test_add.py ...
tests/func/test_add_variety.py ............................
tests/func/test_add_variety2.py ............
tests/func/test_api_exceptions.py .........
tests/func/test_unique_id.py .
tests/unit/test_cli.py .....
tests/unit/test_task.py ....

---------- coverage: platform darwin, python 3.6.2-final-0 -----------

Name                           Stmts   Miss  Cover
--------------------------------------------------
src\tasks\__init__.py              2      0   100%
src\tasks\api.py                  79     22    72%
src\tasks\cli.py                  45     14    69%
src\tasks\config.py               18     12    33%
src\tasks\tasksdb_pymongo.py      74     74     0%
src\tasks\tasksdb_tinydb.py       32      4    88%
--------------------------------------------------
TOTAL                            250    126    50%

================== 62 passed in 0.47 seconds ===================

Поскольку текущий каталог является tasks_proj_v2, а тестируемый исходный код находится в src, добавление опции --cov=src создает отчет о покрытии только для этого тестируемого каталога.


Как видите, некоторые файлы имеют довольно низкий и даже 0%, охват. Это полезные напоминания: tasksdb_pymongo.py 0%, потому что мы отключили тестирование для MongoDB в этой версии. Некоторые из них довольно низкие. Проект, безусловно, должен будет поставить тесты для всех этих областей, прежде чем он будет готов к прайм-тайм.


Я полагаю, что несколько файлов имеют более высокий процент покрытия: api.py и tasksdb_tinydb.py. Давайте посмотрим на tasksdb_tinydb.py и посмотрим, чего не хватает. Думаю, что лучший способ сделать это — использовать отчеты HTML.


Если вы снова запустите coverage.py с параметром --cov-report=html, будет создан отчет в формате HTML:


$ pytest --cov=src --cov-report=html
===================== test session starts ======================
plugins: mock-1.6.2, cov-2.5.1
collected 62 items
tests/func/test_add.py ...
tests/func/test_add_variety.py ............................
tests/func/test_add_variety2.py ............
tests/func/test_api_exceptions.py .........
tests/func/test_unique_id.py .
tests/unit/test_cli.py .....
tests/unit/test_task.py ....
---------- coverage: platform darwin, python 3.6.2-final-0 -----------
Coverage HTML written to dir htmlcov
================== 62 passed in 0.45 seconds ===================

Затем можно открыть htmlcov/index.html в браузере, который показывает вывод на следующем экране:



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



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



Даже если этот экран не является полной страницей для этого файла, этого достаточно, чтобы сказать нам, что:


  1. Мы не тестируем list_tasks() с установленным владельцем.
  2. Мы не тестируем update() или delete().
  3. Возможно, мы недостаточно тщательно тестируем unique_id().

Отлично. Мы можем включить их в наш список TO-DO по тестированию вместе с тестированием системы конфигурации.


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


Более подробную информацию можно найти в документации coverage.py и pytest-cov.


mock: Подмена частей системы


Пакет mock используется для замены частей системы, чтобы изолировать части тестируемого кода от остальной части системы. Mock — объекты иногда называют тестовыми двойниками, шпионами, подделками или заглушками.


Между собственной фикстурой pytest monkeypatch (описанной в разделе Использование monkeypatch на стр. 85) и mock у вас должна быть вся необходимая двойная функциональность теста.


Внимание! Mock-и очень странные
Если вы впервый раз столкнулись с тестовыми двойниками, такими как mocks, stubs, и spies, будьте готовы! Это будет очень странно очень быстро, забавно, хотя и очень впечатляюще.

Пакет mock поставляется стандартной библиотекой Python, как unittest.mock начиная с Python 3.3. В более ранних версиях он доступен в виде отдельного пакета, устанавливаемого через PyPI. Это означает, что вы можете использовать версию mock PyPI с Python 2.6 до последней версии Python и получить ту же функциональность, что и в последнем mock Python. Однако для использования с pytest плагин под названием pytest-mock имеет некоторые особенности, которые делают его моим предпочтительным интерфейсом для mock-системы.


Для проекта Tasks мы будем использовать mock, чтобы помочь нам протестировать интерфейс командной строки. В Coverage.py: определяя, сколько кода тестируется, на стр. 129 вы увидели, что наш файл cli.py вообще не тестировался. Мы начнем исправлять это сейчас. Но давайте сначала поговорим о стратегии.


Первым решением в проекте Tasks было выполнение большей части тестирования функциональности через api.py. Поэтому разумным решением является то, что тестирование командной строки не обязательно должно быть полным функциональным тестированием. Мы можем быть уверены, что система будет работать через CLI, если мы будем мокать уровень API во время тестирования CLI. Это также удобное решение, позволяющее нам посмотреть на моки в этом разделе.


Реализация CLI Tasks использует сторонний пакет интерфейса командной строки Click. Есть много альтернатив для реализации интерфейса командной строки, в том числе модуль, встроенный в Python argparse. Одна из причин, по которой я выбрал Click, заключается в том, что он включает в себя тестовый движок, который помогает нам тестировать приложения Click. Однако код в cli.py, хотя, как мы надеемся, типичен для приложений Click, не очевиден.


Давайте притормозим и установим 3-ю версию Tasks:


$ cd /path/to/code/
$ pip install -e ch7/tasks_proj_v2
...

Successfully installed tasks

В оставшейся части этого раздела вы разработаете несколько тестов для проверки функциональности «list».
Давайте посмотрим его в действии, чтобы понять, что мы собираемся проверить:


Прим.переводчика: В случае использования платформы Windows, я столкнулся с несколькими проблемами при испытании ниже приведенного сеанса.
  1. Должна быть создана папка для базы с именем tasks_db в папке вашего пользователя. Например c:\Users\User_1\tasks_db\
    Иначе, получим -->> FileNotFoundError: [Errno 2] No such file or directory: 'c:\Users\User_1//tasks_db//tasks_db.json'
  2. Используйте двойные кавычки вместо апострофа. Иначе получите ошибку
    'do something great'
    Usage: tasks add [OPTIONS] SUMMARY
    Try "tasks add -h" for help.

    Error: Got unexpected extra arguments (something great')


$ tasks list
ID owner done summary
-- ----- ---- -------

$ tasks add 'do something great'
$ tasks add "repeat" -o Brian
$ tasks add "again and again" --owner Okken

$ tasks list
  ID      owner  done summary
  --      -----  ---- -------
   1            False do something great
   2      Brian False repeat
   3      Okken False again and again

$ tasks list -o Brian
  ID      owner  done summary
  --      -----  ---- -------
   2      Brian False repeat

$ tasks list --owner Brian
  ID      owner  done summary
  --      -----  ---- -------
   2      Brian False repeat

Выглядит довольно просто. Команда tasks list выводит список всех задач под заголовком.
Заголовок печатается, даже если список пуст. Команда вводит на экран только данные от одного владельца, если используются -o или --owner. И как мы это проверим? Способов много, но мы собираемся использовать мОки.


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


ch7/tasks_proj_v2/src/tasks/cli.py

if __name__ == '__main__':
    tasks_cli()

Это просто вызов tasks_cli():


ch7/tasks_proj_v2/src/tasks/cli.py

@click.group(context_settings={'help_option_names': ['-h', '--help']})
@click.version_option(version='0.1.1')
def tasks_cli():
   """Run the tasks application."""
   pass

Очевидно? Нет. Но подождите, это становится хорошим (или плохим, в зависимости от вашей точки зрения). Вот одна из команд-команда list:


ch7/tasks_proj_v2/src/tasks/cli.py

@tasks_cli.command(name="list", help="list tasks")
@click.option('-o', '--owner', default=None,
              help='list tasks with this owner')
def list_tasks(owner):
    """
    Список задач в БД.

    Если указан владелец, список задач только для этого владельцем.
    """
    formatstr = "{: >4} {: >10} {: >5} {}"
    print(formatstr.format('ID', 'owner', 'done', 'summary'))
    print(formatstr.format('--', '-----', '----', '-------'))
    with _tasks_db():
        for t in tasks.list_tasks(owner):
            done = 'True' if t.done else 'False'
            owner = '' if t.owner is None else t.owner
            print(formatstr.format(
                  t.id, owner, done, t.summary))

Когда вы привыкнете писать код Click, то убедитесь, что этот код не так уж плох. Я не собираюсь объяснять здесь, что и как в этой функции работает, так как разработка кода командной строки не является фокусом книги; однако, хотя я почти абсолютно уверен, что у меня есть этот правильный код, всё равно, всегда есть много места для человеческой ошибки. Вот почему важен хороший набор автоматических тестов, чтобы убедиться, что эта функция работает правильно.
Эта функция list_tasks(owner) зависит от нескольких других функций: tasks_db(), которая является менеджером контекста, и tasks.list_tasks(owner), которая является функцией API.


Мы собираемся использовать mock, чтобы поставить поддельные функции на место для tasks_db() и tasks.list_tasks(). Затем мы можем вызвать метод list_tasks через интерфейс командной строки и убедиться, что он вызывает функцию tasks.list_tasks(), которая работает правильно и корректно обрабатывает возвращаемое значение.
Чтобы заглушить tasks_db(), давайте посмотрим на реальную реализацию:


ch7/tasks_proj_v2/src/tasks/cli.py

@contextmanager
def _tasks_db():
    config = tasks.config.get_config()
    tasks.start_tasks_db(config.db_path, config.db_type)
    yield
    tasks.stop_tasks_db()

Функция tasks_db() — это менеджер контекста, который получает конфигурацию из tasks.config.get_config(), другой внешней зависимости, и использует конфигурацию для установления соединения с базой данных. yield передает управление блоку list_tasks(), и после того, как все сделано, соединение с базой данных прекращается.


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


ch7/tasks_proj_v2/tests/unit/test_cli.py

from click.testing import CliRunner
from contextlib import contextmanager
import pytest
from tasks.api import Task
import tasks.cli
import tasks.config

@contextmanager
def stub_tasks_db():
    yield

Этот импорт предназначен для тестов. Единственный импорт, необходимый для заглушки — from contextlib import contextmanager.


Мы будем использовать mock, чтобы заменить настоящий менеджер контекста нашей заглушкой. На самом деле, мы будем использовать mocker, который является фикстурой плагина pytest-mock. Давайте посмотрим на реальный тест. Вот тест, который вызывает список задач :


ch7/tasks_proj_v2/tests/unit/test_cli.py

def test_list_no_args(mocker):
    mocker.patch.object(tasks.cli, '_tasks_db', new=stub_tasks_db)
    mocker.patch.object(tasks.cli.tasks, 'list_tasks', return_value=[])
    runner = CliRunner()
    runner.invoke(tasks.cli.tasks_cli, ['list'])
    tasks.cli.tasks.list_tasks.assert_called_once_with(None)

Фикстура mocker предоставляется pytest-mock-ом в качестве удобного интерфейса для unittest.mock. Первая строка,mocker.patch.object(tasks.cli, 'tasks_db', new=stub_tasks_db), заменяет менеджер контекста tasks_db() заглушкой, которая ничего не делает.


Вторая строка, mocker.patch.object(tasks.cli.tasks, 'list_tasks', return_value=[]), заменяет любые вызовы tasks.list_tasks() из tasks.cli на объект MagicMock по умолчанию возвращаемым значением пустого списка. Мы можем использовать этот объект позже, чтобы увидеть, был ли он вызван правильно. Класс MagicMock-это гибкий подкласс unittest.mock с разумным поведением по умолчанию и возможностью указать возвращаемое значение, которое мы используем в этом примере. Классы Mock и MagicMock (и другие) используются для имитации интерфейса другого кода с помощью встроенных методов самоанализа, чтобы можно было спросить их, как они были вызваны.


Третья и четвертая строки test_list_no_args() используют Click CliRunner, чтобы сделать то же самое, что и вызов списка задач в командной строке.


Последняя строка использует макет объекта, чтобы убедиться, что вызов API был вызван правильно. Метод assert_called_once_with() является частью объектов unittest.mock.Mock, которые перечислены в документации по Python.


Давайте посмотрим на почти идентичную тестовую функцию, которая проверяет вывод:


ch7/tasks_proj_v2/tests/unit/test_cli.py

@pytest.fixture()
def no_db(mocker):
    mocker.patch.object(tasks.cli, '_tasks_db', new=stub_tasks_db)

def test_list_print_empty(no_db, mocker):
    mocker.patch.object(tasks.cli.tasks, 'list_tasks', return_value=[])
    runner = CliRunner()
    result = runner.invoke(tasks.cli.tasks_cli, ['list'])
    expected_output = ("  ID      owner  done summary\n"
                       "  --      -----  ---- -------\n")
    assert result.output == expected_output

На этот раз мы поместили фиктивную заглушку tasks_db в фикстуру no_db, чтобы её было легче использовать в будущих тестах. tasks.list_tasks() такой же, как и раньше. Однако, на этот раз, мы также проверяем выходные данные действия командной строки через result.output и ассертим равенство к ожидаемому результату expected_output.


Этот ассерт можно было бы поместить в первый тест test_list_no_args, тем самым мы могли бы устранить необходимость в двух тестах. Тем не менее, я меньше верю в свою способность получить правильный код CLI здесь, чем в другом коде, поэтому разделяю вопросы: «Правильно ли вызывается API?» и «Является ли вывод на печать правильным?» в двух тестах кажется целесообразным.


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


ch7/tasks_proj_v2/tests/unit/test_cli.py

def test_list_print_many_items(no_db, mocker):
    many_tasks = (
        Task('write chapter', 'Brian', True, 1),
        Task('edit chapter', 'Katie', False, 2),
        Task('modify chapter', 'Brian', False, 3),
        Task('finalize chapter', 'Katie', False, 4),
    )
    mocker.patch.object(tasks.cli.tasks, 'list_tasks',
                        return_value=many_tasks)
    runner = CliRunner()
    result = runner.invoke(tasks.cli.tasks_cli, ['list'])
    expected_output = ("  ID      owner  done summary\n"
                       "  --      -----  ---- -------\n"
                       "   1      Brian  True write chapter\n"
                       "   2      Katie False edit chapter\n"
                       "   3      Brian False modify chapter\n"
                       "   4      Katie False finalize chapter\n")
    assert result.output == expected_output

def test_list_dash_o(no_db, mocker):
    mocker.patch.object(tasks.cli.tasks, 'list_tasks')
    runner = CliRunner()
    runner.invoke(tasks.cli.tasks_cli, ['list', '-o', 'brian'])
    tasks.cli.tasks.list_tasks.assert_called_once_with('brian')

def test_list_dash_dash_owner(no_db, mocker):
    mocker.patch.object(tasks.cli.tasks, 'list_tasks')
    runner = CliRunner()
    runner.invoke(tasks.cli.tasks_cli, ['list', '--owner', 'okken'])
    tasks.cli.tasks.list_tasks.assert_called_once_with('okken')

Давайте убедимся, что всё это работает:


$ cd /path/to/code/ch7/tasks_proj_v2
$ pytest -v tests/unit/test_cli.py
=================== test session starts ===================
plugins: mock-1.6.2, cov-2.5.1
collected 5 items
tests/unit/test_cli.py::test_list_no_args PASSED
tests/unit/test_cli.py::test_list_print_empty PASSED
tests/unit/test_cli.py::test_list_print_many_items PASSED
tests/unit/test_cli.py::test_list_dash_o PASSED
tests/unit/test_cli.py::test_list_dash_dash_owner PASSED
================ 5 passed in 0.06 seconds =================

Ура! Они проходят.


Это был чрезвычайно быстрый способ знакомства с использованием тестовых двойников и mocks. Если вы хотите использовать mocks в своем тестировании, я рекомендую вам прочитать о unittest.mock в документации стандартной библиотеки https://docs.python.org/dev/library/unittest.mock.html и о pytest-mock на pypi.python.org.


tox: Тестирование Различных Конфигураций


tox-это инструмент командной строки, который позволяет запускать полный набор тестов в нескольких средах. Мы собираемся использовать его для тестирования проекта Tasks в нескольких версиях Python. Однако tox не ограничивается только версиями Python. Вы можете использовать его для тестирования с различными конфигурациями зависимостей и различными конфигурациями для различных операционных систем.


В общих чертах, вот ментальная модель того, как работает tox:


tox использует setup.py файл для тестируемого пакета, чтобы создать установочный исходный дистрибутив вашего пакета. Это похоже на tox.ini для списка сред, а затем для каждой среды…


  1. tox создает виртуальную среду в каталоге .tox.
  2. tox pip устанавливает некоторые зависимости.
  3. tox pip устанавливает ваш пакет из sdist на шаге 1.
  4. tox запускает ваши тесты.

После того, как все среды протестированы, tox создаст отчёт с результатами.


Эти слова обретут больше смысла, когда вы увидите его в действии, поэтому давайте посмотрим, как изменить проект Tasks, чтобы использовать tox для тестирования Python 2.7 и 3.6. Я выбрал версии 2.7 и 3.6, так как они уже установлены в моей системе.
Если у вас установлены разные версии, измените строку envlist в соответствии с той версией, которую вы хотите или хотите установить.
Первое, что нам нужно сделать для проекта Tasks, — это добавить файл tox.ini на том же уровне, что и setup.py — верхний каталог проекта. Я также собираюсь переместить все, что находится в pytest.ini, в tox.ini.
Вот сокращенное расположение кода:


    tasks_proj_v2/
    ├── ...
    ├── setup.py
    ├── tox.ini
    ├── src
    │    └── tasks
    │          ├── __init__.py
    │          ├── api.py
    │          └── ...
    └── tests
          ├── conftest.py
          ├── func
          │    ├── __init__.py
          │    ├── test_add.py
          │    └── ...
          └── unit
               ├── __init__.py
               ├── test_task.py
               └── ...

Теперь вот как выглядит файл tox.ini:


ch7/tasks_proj_v2/tox.ini

# tox.ini , положить в тот же каталог, что и setup.py

[tox]
envlist = py27,py36

[testenv]
deps=pytest
commands=pytest

[pytest]
addopts = -rsxX -l --tb=short --strict
markers = 
  smoke: Run the smoke test test functions
  get: Run the test functions that test tasks.get()

В [tox] у нас есть envlist = py27, py36. Это сокращение, позволяющее tox запускать наши тесты с использованием python2.7 и python3.6.
Под [testenv] строка deps = pytest указывает tox, чтобы убедиться, что pytest установлен. Если у вас есть несколько тестовых зависимостей, вы можете поместить их в отдельные строки.
Вы также можете указать, какую версию использовать.
Строка command = pytest указывает tox запускать pytest в каждой среде.
В [pytest] мы можем поместить все, что мы обычно хотели бы поместить в pytest.ini для настройки pytest, как описано в Главе 6, Конфигурация, на странице 113. В этом случае addopts используется для включения дополнительной сводной информации для пропусков xfails и xpasses (-rsxX) и включить отображение локальных переменных в трассировке стека (-l). Он также по умолчанию использует сокращенные трассировки стека (--tb = short) и гарантирует, что все маркеры, используемые в тестах, будут объявлены первыми (--strict). Раздел маркеров — то, где маркеры объявлены.
Перед запуском tox вы должны убедиться, что вы установили его:


$ pip install tox

Это можно сделать в виртуальной среде.
Чтобы запустить tox, просто вызовите tox:


$ cd /path/to/code/ch7/tasks_proj_v2
$ tox

GLOB sdist-make: /path/to/code/ch7/tasks_proj_v2/setup.py
py27 create: /path/to/code/ch7/tasks_proj_v2/.tox/py27
py27 installdeps: pytest
py27 inst: /path/to/code/ch7/tasks_proj_v2/.tox/dist/tasks-0.1.1.zip
py27 installed: click==6.7,funcsigs==1.0.2,mock==2.0.0,
                pbr==3.1.1,py==1.4.34,pytest==3.2.1,
                pytest-mock==1.6.2,six==1.10.0,tasks==0.1.1,tinydb==3.4.0
py27 runtests: PYTHONHASHSEED='1311894089'
py27 runtests: commands[0] | pytest
================= test session starts ==================
plugins: mock-1.6.2
collected 62 items

tests/func/test_add.py ...
tests/func/test_add_variety.py ............................
tests/func/test_add_variety2.py ............
tests/func/test_api_exceptions.py .........
tests/func/test_unique_id.py .
tests/unit/test_cli.py .....
tests/unit/test_task.py ....
============== 62 passed in 0.25 seconds ===============
py36 create: /path/to/code/ch7/tasks_proj_v2/.tox/py36
py36 installdeps: pytest
py36 inst: /path/to/code/ch7/tasks_proj_v2/.tox/dist/tasks-0.1.1.zip
py36 installed: click==6.7,py==1.4.34,pytest==3.2.1,
                pytest-mock==1.6.2,six==1.10.0,tasks==0.1.1,tinydb==3.4.0
py36 runtests: PYTHONHASHSEED='1311894089'
py36 runtests: commands[0] | pytest
================= test session starts ==================
plugins: mock-1.6.2
collected 62 items

tests/func/test_add.py ...
tests/func/test_add_variety.py ............................
tests/func/test_add_variety2.py ............
tests/func/test_api_exceptions.py .........
tests/func/test_unique_id.py .
tests/unit/test_cli.py .....
tests/unit/test_task.py ....
============== 62 passed in 0.27 seconds ===============
_______________________ summary ________________________
  py27: commands succeeded
  py36: commands succeeded
  congratulations :)

В конце концов, у нас есть хорошее резюме всех тестовых сред и их результатов:


_______________________ summary ________________________
  py27: commands succeeded
  py36: commands succeeded
  congratulations :)

Разве это не замечательно? Мы получили «поздравления» и смайлик.


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


Jenkins CI: Автоматизация ваших автоматических тестов


Системы непрерывной интеграции (CI), такие как Jenkins, часто используются для запуска тестовых пакетов после каждой фиксации кода. В pytest есть опции для генерации файлов в формате junit.xml, которые требуются Jenkins и другим системам CI для отображения результатов тестирования.


Jenkins — это сервер автоматизации с открытым исходным кодом, который часто используется для непрерывной интеграции. Несмотря на то, что Python не нужно компилировать, довольно распространенной практикой является использование Jenkins или других систем CI для автоматизации запуска и создания отчетов по проектам Python. В этом разделе вы узнаете, как проект Tasks может быть настроен в Jenkins. Я не собираюсь проходить через установку Jenkins. Это отличается для каждой операционной системы, и инструкции доступны на веб-сайте Jenkins.
При использовании Jenkins для запуска наборов pytest есть несколько плагинов Jenkins, которые могут оказаться полезными. Они были установлены для примера:


  • build-name-setter: Этот плагин устанавливает отображаемое имя сборки, отличное от #1, #2, #3 и т.д.
  • Test Results Analyzer plugin: Этот плагин показывает историю результатов выполнения теста в табличном или графическом формате.

Вы можете установить плагины, перейдя на заглавную страницу Jenkins, которая расположена лично у меня по адресу localhost:8080/manage, так как я запускаю её локально, а затем нажмите Manage Jenkins -> Manage Plugins -> Available. Найдите нужный плагин с помощью поля фильтра. Установите флажок для плагина, который вы хотите. Я обычно выбираю «Install without Restart» (Установить без перезапуска), а затем на странице «Installing Plugins/Upgrades» я выбираю поле с надписью «Restart Jenkins when installation is complete and no jobs are
running»(Перезапустить Jenkins, когда установка завершена и задания не выполняются).


Мы рассмотрим полную конфигурацию на тот случай, если вы захотите и дальше следовать проекту Tasks. Jenkins project/item «Freestyle Project» с именем «tasks», как показано на следующем экране.



Конфигурация немного странная, так как мы используем версии проекта Tasks, которые выглядят как tasks_proj, tasks_proj_v2 и так далее, а не систему контроля версий.


Поэтому нам необходимо параметризовать проект, чтобы каждый сеанс тестирования указывал, где устанавливать проект Tasks и где искать тесты. Мы будем использовать несколько строковых параметров, как показано на следующем экране, чтобы указать эти каталоги.
(Нажмите «This project is parametrized»(Параметры текущего проекта), чтобы получить доступ к этим опциям.)



Далее, прокрутите вниз до «Build Environment», выберите «Delete workspace before build starts»(Удалить рабочее пространство перед началом сборки) и «Set Build Name». Установите имя $ {start_tests_dir} #${BUILD_NUMBER}, как показано на следующем экране.



Далее идут шаги сборки. В Mac или Unix-подобных системах выберите Add build step-> Execute shell. В Windows выберите Add build step->Execute Windows batch command. Поскольку я работаю на Mac, я использовал блок оболочки Execute для вызова скрипта, как показано здесь:



Содержимое текстового поля:
# ваши пути будут другими
code_path=/Users/okken/projects/book/bopytest/Book/code
run_tests=${code_path}/ch7/jenkins/run_tests.bash
bash -e ${run_tests} ${tasks_proj_dir} ${start_tests_dir} ${WORKSPACE}


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


Вот сценарий:


ch7/jenkins/run_tests.bash

#!/bin/bash
# ваши пути будут другими
top_path=/Users/okken/projects/book/bopytest/Book
code_path=${top_path}/code
venv_path=${top_path}/venv
tasks_proj_dir=${code_path}/$1
start_tests_dir=${code_path}/$2
results_dir=$3
# click и Python 3,
# из http://click.pocoo.org/5/python3/
export LC_ALL=en_US.utf-8
export LANG=en_US.utf-8
# виртуальная среда
source ${venv_path}/bin/activate
# установить проект
pip install -e ${tasks_proj_dir}
# запустить тесты
cd ${start_tests_dir}
pytest --junit-xml=${results_dir}/results.xml

В нижней строке есть pytest --junit-xml = $ {results_dir} /results.xml. Флаг --junit-xml — это единственное, что необходимо для создания отчета формата junit.xml, в котором нуждается Jenkins.


Есть и другие варианты:


$ pytest --help | grep junit
--junit-xml=path       создать файл отчета в стиле junit-xml по заданному пути.
--junit-prefix=str     добавить префикс к именам классов в выводе junit-xml
junit_suite_name (string) имя набора тестов для отчета JUnit

--junit-prefix может использоваться как префикс для каждого теста. Это полезно при использовании tox, и вы хотите разделить различные результаты среды. junit_suite_name — это опция файла конфигурации, которую вы можете установить в разделе [pytest] pytest.ini или tox.ini.
Позже мы увидим, что результаты будут содержать (from pytest) внутри. Чтобы изменить pytest на что-то другое, используйте junit_suite_name.
Далее мы добавим действие после сборки: Add post-build action(Добавить действие после сборки)->Publish Junit test result report(Опубликовать отчет о результатах теста Junit).
Заполните информацию XML отчета о тестировании файлом results.xml, как показано на следующем экране.



Вот и все! Теперь мы можем провести тесты через Jenkins. Вот эти шаги:


  1. Нажмите Сохранить.
  2. Перейти к началу проекта.
  3. Нажмите «Build with Parameters» (Построить с параметрами).
  4. Выберите ваши каталоги и нажмите Build.
  5. Когда всё будет сделано, наведите курсор мыши на заголовок рядом с мячиком в истории сборки и
    выберите Console Output из раскрывающегося меню. (Или щелкните имя сборки и выберите «Вывод на консоль».)
  6. Посмотрите на вывод и попробуйте выяснить, что пошло не так.

Вы можете пропустить шаги 5 и 6, но я никогда этого не сделаю. Я никогда бы не стал изголяться с Jenkins если бы всё работало с первого раза. В моем сценарии обычно возникают проблемы с правами доступа к каталогу, путями или опечатками и так далее.
Прежде чем мы посмотрим на результаты, давайте запустим еще одну версию, чтобы сделать её интересной.
Снова нажмите «Построить с параметрами». На этот раз сохраните ту же директорию проекта, но установите ch2 в качестве start_tests_dir и нажмите Build. После обновления вида сверху проекта вы должны увидеть следующий экран:



Щелкните внутри графика или по ссылке «Последний результат теста», чтобы увидеть обзор сеанса тестирования со значками «+», чтобы развернуть его в случае неудачи теста.
Если щелкнуть любое из названий тестов с ошибками, вы увидите информацию об ошибках отдельных тестов, как показано на следующем экране. Здесь вы видите «(from pytest)» как часть названия теста. Это то, что контролируется junit_suite_name в файле конфигурации.



Возвращаясь к Jenkins > tasks, вы можете нажать на Test Results Analyzer, чтобы увидеть представление, которое показывает, какие тесты не выполнялись для разных сессий, а также состояние прокатило/непрокатило (см. Следующий экран):



Вы видели, как запускать pytest suites с виртуальными средами от Jenkins, но есть довольно много других тем для изучения, связанных с использованием pytest и Jenkins вместе. Вы можете протестировать несколько сред с помощью Jenkins, либо установив отдельные задачи Jenkins для каждой среды, либо попросив Jenkins вызвать tox напрямую. Есть также хороший плагин под названием Cobertura, который способен отображать данные покрытия из coverage.py. ознакомьтесь с документацией Jenkins за дополнительной информацией.


Вернуться

Теги:
Хабы:
+12
Комментарии1

Публикации

Истории

Работа

Data Scientist
63 вакансии
Python разработчик
142 вакансии

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн