Архив метки: pytest

pytest продолжение

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

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

Последовательно выполнение тестовых сценариев


В тестировании часто бывают случаи, когда необходимо объединить несколько проверок в один большой тест и выполнить их последовательно. Причем бывает и так, что один сценарий зависит от результатов работы предыдущего (но такое лучше стараться избегать в автотестировании). 

В следующих статьях мы посмотрим как можно избежать этой ситуации пользуясь стандартными средствами пайтеста.

Например, тестируемая программа при первом запуске накатывает схему базы (sqlite чтобы не морочиться) и заполняет её некоторой служебной информацией. Допустим перед нами стоит задача «дешево» протестировать этот модуль. Сходу задачу модно разбить на 2 подзадачи — проверка создания файла базы, затем проверка схемы и в конце проверка данных.

Самый простой вариант — нумерация в названии тестов

# conftest.py
import os
import pytest
import sqlite3


def pytest_configure(config):

    config.DB_PATH = «db.sqlite»


@pytest.fixture(scope=»module»)
def creator(request):
    DB_PATH = request.config.DB_PATH
    con = sqlite3.connect(DB_PATH)
    cur = con.cursor()
    cur.executescript(«»»
        create table person(
            firstname,
            lastname,
            age
        );

        create table book(
            title,
            author,
            published
        );

        insert into book(title, author, published)
        values (
            'Dirk Gently''s Holistic Detective Agency',
            'Douglas Adams',
            1987
        );
        «»»)
    con.commit()
    con.close()
    return DB_PATH


@pytest.fixture
def cursor(request):
    class cursor:

        def __init__(self, dbpath):
            self.dbpath = dbpath
            self.conn = sqlite3.connect(self.dbpath)

        def table_exists(self, table):
            cursor = self.conn.cursor()
            cursor.execute(
                «SELECT name FROM sqlite_master WHERE type = «table»»)
            tables = cursor.fetchall()
            for each in tables:
                if table in each:
                    return True
            return False

        def getrows(self, table):
            cursor = self.conn.cursor()
            cursor.execute(«SELECT * FROM %s;» % (table))
            return cursor.fetchall()

    return cursor(request.config.DB_PATH)

# test.py
def test_1_db_exists(creator):
    assert os.path.exists(creator)


@pytest.mark.parametrize(«table», [«person», «book»])
def test_2_check_scheme(table, creator, cursor):
    result = cursor.table_exists(table)
    assert result


@pytest.mark.parametrize(«table», [«book»])
def test_3_check_rows(table, creator, cursor):
    assert cursor.getrows(table)


Спасибо документации питона за готовые примеры.

У такого подхода есть ряд недостатков.
1. В названии теста появляется нумерация — это лишняя мета информация. (Это может сказаться на тестовом отчете или билд логе CI сервера)
2. Название теста усложняется
3. Вставка нового элемента в середину приведет к смене нумерации всех последующих тестов

Вариант посложнее — маркеры


Свои маркеры мы реализовывать не будем, а используем готовый плагин pytest-ordering. В принципе вся реализация такого плагина составит не больше 100 строчек кода.

# test.py
@pytest.mark.order1
def test_db_exists(creator):
    assert os.path.exists(creator)


@pytest.mark.order2
@pytest.mark.parametrize(«table», [«person», «book»])
def test_check_scheme(table, creator, cursor):
    result = cursor.table_exists(table)
    assert result


@pytest.mark.order3
@pytest.mark.parametrize(«table», [«book»])
def test_check_rows(table, creator, cursor):
    assert cursor.getrows(table)


Наши тестовые функции преобразились, стали более понимаемы для чтения.
Из коробки плагин содержит создание цепочек выполнения, более понятные маркеры (pytest.mark.run('first'), pytest.mark.run('second')) и прочее. Более подробно в микро доку плагина.

Ссылки
[1] https://docs.python.org/2/library/sqlite3.html
[2] http://pytest-ordering.readthedocs.org/en/develop/

Автор: Евгений Курочкин

Часть 4. py.test debug

Мы уже рассмотрели основные принципы написание тестов, но один важный момент упустили, а именно дебаг тестов.

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

Часто люди пользующие питон дебажат свои скрипты с помощью использования принтов. По умолчанияю pytest пишет весь stout и stderr. В предыдущем посте эта тема рассматривалась. Для отключения этой особенности нужно использовать параметр —capture или маску -s. 
Посмотрим на небольшой (надуманный) пример:

# use_debug.py
@py.test.mark.parametrize(«v», range(1, 5))
def test_parity(v):
    print(«Value: %d» % v)
    assert v % 2

>>>py.test use_debug.py -s -v —tb line

============================= test session starts =============================
platform win32 — Python 2.7.5 — py-1.4.20 — pytest-2.5.2 — C:Python27python.exe
plugins: teamcity-messages, rerunfailures
collected 4 items

use_debug.py:40: test_parity[1] Value: 1
PASSED
use_debug.py:40: test_parity[2] Value: 2
FAILED
use_debug.py:40: test_parity[3] Value: 3
PASSED
use_debug.py:40: test_parity[4] Value: 4
FAILED

================================== FAILURES ===================================
c:JOB3_TestSolutionBloguse_debug.py:43: assert (2 % 2)
c:JOB3_TestSolutionBloguse_debug.py:43: assert (4 % 2)
===================== 2 failed, 2 passed in 0.03 seconds ======================

Как видим в консоли напечатались входные значения. 


Автовключение дебага

Для этого предусмотрен флаг —pdb. Если тест пофелился, то py.test прогонит его на тех же данных, но уже с включенным питоновским дебагером pdb.

>>>py.test use_debug.py -s -v —tb line —pdb

platform win32 — Python 2.7.5 — py-1.4.20 — pytest-2.5.2 — C:Python27python.exe
plugins: teamcity-messages, rerunfailures
collected 4 items

use_debug.py:64: test_parity[1]
>>>>>>>>>>>>>>>>>>> PDB set_trace (IO-capturing turned off) >>>>>>>>>>>>>>>>>>>
> c:job3_testsolutionbloguse_debug.py(67)test_parity()
-> assert v % 2
(Pdb)

Брейкпоинты


Из коробки py.test предоставляет возможность в коде явно указать брекпоинт.

# use_debug.py
@py.test.mark.parametrize(«v», range(1, 5))
def test_parity(v):
    py.test.set_trace()
    assert v % 2

При каджом вызове тестовой функции test_parity в месте устанвки брейкпоинта py.test будет запускать pdb

>>>py.test use_debug.py -s -v —tb line

use_debug.py:62: test_v[1]
>>>>>>>>>>>>>>>>>>> PDB set_trace (IO-capturing turned off) >>>>>>>>>>>>>>>>>>>
> c:job3_testsolutionbloguse_debug.py(65)test_v()
-> assert v % 2
(Pdb)

Ссылки

http://pytest.org/latest/usage.html#dropping-to-pdb-python-debugger-on-failures
http://pytest.org/latest/usage.html#setting-a-breakpoint-aka-set-trace

Автор: Евгений Курочкин

Часть 3. Совсем немного о py.test.exe

py.test.exe это тул призванный упростить запуск тестов. Оговорюсь сразу что в мою задачу не входит перевод документации, поэтому ознакомится с полным набором команд можно, выполнив в командной строке следующую комманду (да и он может различаться в зависимости от того какие установлены плагины) и пройти по сслылкам в конце поста.

>>> py.test -h

Команды формирования вывода

-q — уменьшает детализацию информации о выполнении теста

-v — увеличивает детализацию о выполнении теста

—capture — параметр для управлением захвата вывода stdout/stderr. Принимает одно из значений fd|sys|no. Более подробно в ссылках
-s — маска для —capture=no. Параметр удобен, если нужно убрать мусор (например, дебажные принты) из информации о выполнившихся тестах.

—tb — отвечает за детализацию вывода трейса. Одно из long/short/line/native/no

Команды запуска 

-k — выполнить лишь заданный тест

—maxfail= — задает максимальное число фейлов, тест удобен при дебаге или если заранее известно что результат тестов будет один и тотже
-x — маска дял —maxfaile=1

—pdb — стартует питоновский дебагер для отладки ошибок. Удобен для отладки тестов (подробнее в одном из следующих постов о дебаге тестов)

Комманды для настройки

—color (yes|no|auto) — цветной вывод в консоли или нет

Совсем небольшой пример

# use_tool.py
import py.test

@py.test.mark.parametrize('num', [i for i in range(5)])
def test_func(num):
    print('ntest_func()')
    assert num % 2

def test_fail():
    print('bla, bla, fail')
    assert False

Выполняем в консоли
>>>py.test use_tool.py -k «test_func» -v -s -x —tb=line

============================= test session starts =============================
platform win32 — Python 2.7.5 — py-1.4.20 — pytest-2.5.2 — C:Python27python.exe
plugins: teamcity-messages
collected 6 items

use_tool.py:37: test_func[0]
test_func()
FAILED

================================== FAILURES ===================================
use_tool.py:40: assert (0 % 2)
!!!!!!!!!!!!!!!!!!! Interrupted: stopping after 1 failures !!!!!!!!!!!!!!!!!!!!
===================== 1 tests deselected by '-ktest_func' =====================
=================== 1 failed, 1 deselected in 0.04 seconds ====================

Ссылки

[1] http://pytest.org/latest/capture.html
[2] http://pytest.org/latest/customize.html?highlight=maxfail
[3] http://pytest.org/latest/usage.html

Автор: Евгений Курочкин

Часть 2. Фикстуры pytest

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

Что такое test fixtures

В тестировании обычно под этим понимают флоу теста, его контекст.
В py.test предусмотрены следующие типы фикстур: встроенные фикстуры, вспомогательные функции, декораторы

setup/teardown

Часто перед запуском теста нужно провести базовые подготовления. Например, это может быть настройка окружения, запуск тестовых сервисов, подготовка тестовых данных, инициализация соединения с базами и проч. Для выполнения этих операций в py.test входит небольшой набор вспомогательных функций — несколько пар setup/teardown.

setup_module(module)
teardown_module(module)

Это функции которые вызываются перед и после выполнения всех тестов (в прошлой части более подробно об организации тестов)
Данную пару удобно использовать когда подготовка флоу происходит однажды — перед  запуском всех тестов.

setup_function(function)
teardown_function(function)

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

#fixtures.py
def setup_module(module):
    print('nsetup_module()')


def teardown_module(module):
    print('nteardown_module()')


def setup_function(function):
    print('nsetup_function()')


def teardown_function(function):
    print('nteardown_function()')


def test_first():
    print('ntest_first()')


def test_second():
    print('ntest_second()')


class TestMyClass:
    @classmethod 
    def setup_class(cls):
        print ('nsetup_class()')

    @classmethod 
    def teardown_class(cls):
        print ('teardown_class()')

    def setup_method(self, method):
        print ('nsetup_method()')

    def teardown_method(self, method):
        print ('nteardown_method()')

    def test_first(self):
        print('test_first()')

    def test_second(self):
        print('test_second()')



>>>py.test -s -v fixtures.py

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

platform win32 — Python 2.7.5 — py-1.4.20 — pytest-2.5.2 — C:Python27python.exe
collected 4 items

fixtures.py:17: test_first
setup_module()

setup_function()

test_first()
PASSED
teardown_function()

fixtures.py:21: test_second
setup_function()

test_second()
PASSED
teardown_function()

fixtures.py:40: TestMyClass.test_first
setup_class()

setup_method()
test_first()
PASSED
teardown_method()

fixtures.py:43: TestMyClass.test_second
setup_method()
test_second()
PASSED
teardown_method()
teardown_class()

teardown_module()
========================== 4 passed in 0.02 seconds ===========================

Встроенные фикстуры

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

>>>py.test —fixtures

Для ознакомления в данном посте рассмотри одну из встроенных фикстур tempdir
temdir
Очень полезная вещь, предназначена для работы с темповыми данными. Т.е. в темповой директории можно хранить любые временные файлы. Например, это могут быть логи тестируемых программ или какой-то контекcт теста, который может понадобиться для воспроизведения бага.

Использовать крайне просто

#fixtures.py
def test_file(tmpdir):
    f = tmpdir.mkdir('logs').join('test.log')
    print(tmpdir.strpath)
    f.write('bla bla')

    with open(f.strpath, 'r') as fp:
        print(fp.read())

>>>py.test -s -v fixtures.py
============================= test session starts =============================
platform win32 — Python 2.7.5 — py-1.4.20 — pytest-2.5.2 — C:Python27python.exe
collected 1 items

fixtures.py:161: test_file temppytest-76test_file0
bla bla
PASSED

========================== 1 passed in 0.03 seconds ===========================

Полезные фикстуры

Для итерации по входным данным можно использовать полезный декоратор py.test.mark.parametrize

#fixtures.py
@py.test.mark.parametrize('num', [i for i in range(2)])
def test(num):
    assert num % 2

py.test -s -v fixtures.py
============================= test session starts =============================
platform win32 — Python 2.7.5 — py-1.4.20 — pytest-2.5.2 — C:Python27python.exe
collected 2 items

fixtures.py:182: test[0] FAILED
fixtures.py:182: test[1] PASSED

================================== FAILURES ===================================
___________________________________ test[0] ___________________________________

num = 0

    @py.test.mark.parametrize('num', [i for i in range(2)])
    def test(num):
>       assert num % 2
E       assert (0 % 2)

fixtures.py:184: AssertionError
===================== 1 failed, 1 passed in 0.02 seconds ======================

Кастомизация

О кастомных фикстурах более подробно в одном из следующих постов, поэтому здесь совсем небольшой пример.

Для определения собственных инструментов в py.test предусмотрены 

#fixtures.py
@pytest.fixture(scope=»function»)
def before(request):
    print('nbefore()')

    def after():
        print(«nafter()»)
    request.addfinalizer(after)


def test_func(before):
    print(«ntest_func()»)



>>>py.test -s -v fixtures.py
============================= test session starts =============================
platform win32 — Python 2.7.5 — py-1.4.20 — pytest-2.5.2 — C:Python27python.exe
collected 1 items

fixtures.py:14: test_func
before()

test_func()
PASSED
after()
========================== 1 passed in 0.01 seconds ===========================

Вывод

1. Прежде всего это создано для удобства (как собственно и весь py.test)
2. Сокращает время разработки теста
3. делает тест прозрачнее
4. Уменьшает количество копипаста

Ссылки

http://pytest.org/latest/
http://en.wikipedia.org/wiki/Test_fixture
http://pythontesting.net/

Автор: Евгений Курочкин

Часть 1. pytest. Первый взгляд

Каждый девелопер, для которого слова tdd, unittest, ci являются не пустыми, слышал про тестовый фраймворк pytest (он же py.test). По каким-то причинам этот тестовый фраймвор слабо освещен на просторах рунета. В ряде статей постараюсь заполнить этот пробел. Думаю, познакомившись с pytest поближе вы забудете про стандартный unittest.



Преимущества pytest

  • он простой
  • он функциональный, в коробке идет большое количество киллер фич
  • он логичный
  • он расширяемый
  • он умный, можно запускать тесты на unittest, doctest
  • код тестов меньше и проще

Ставим


pip install -U pytest
Или через easy_install
easy_install -U pytest

Или из пакета
Качаем пакет http://pypi.python.org/pypi/pytest

python pytest/setup.py install

Единственно при оффлайн установке стоит учесть что для работы pytest нужно еще 2 пакета — colorama и py

Пробуем


Забегая в перед скажу что все тестовые функции должны начинаться с префикса test_*.

# tests.py
def test_first_our_passed():
    assert True

def test_first_our_failed():
    assert False

Запустим тест в консоли py.test test.py

============================= test session starts =============================
platform win32 — Python 2.7.5 — py-1.4.20 — pytest-2.5.2
collected 2 items

tests_first.py .F

================================== FAILURES ===================================
____________________________ test_first_our_failed ____________________________

    def test_first_our_failed():
>       assert False
E       assert False

tests_first.py:6: AssertionError
===================== 1 failed, 1 passed in 0.13 seconds ======================

Из коробки получаем довольно информативный вывод


Структура проекта


Есть два устоявшихся принципа по организации тестового проекта
Все тесты находятся в папке test рядом с пакетами проекта

/myproj
    __init__.py
    project.py
/test
    test_func_1.py
    test_func_2.py

Второй вариант — папка с тестами находится в самом пакете проекта

/myproj
    __init__.py
    project.py
    /test
        test_func_1.py
        test_func_2.py

Разница не большая, а скорее дело вкуса.


Структура тестов


В принципа здесь все банально. Для рассмотрения будем использовать простой тестовый проект.

/myproj
    __init__.py
    project.py

# project.py
def pp(val):
    return val + 1

Первый принцип — все тесты описаны в виде тестовых функциях

def test_positive():
    assert pp(1) == 2

def test_negative():
    assert pp(1) == 1

Второй принцип — все тесты описаны в виде методов тестового класса

class TestProject:
    def test_positive(self):
        assert pp(1) == 2

    def test_negative(self):
        assert pp(1) == 1

На тестах данные принцип практически не сказывается.

Варианты запуска

Из коробки в pytest предусмотрено несколько вариантов запуска тестов.

Через специальную утилиту py.test.exe

py.test.exe test_project.py

В консоли через питон 

python -m «pytest.main('test directory')»

Вызов pytest в коде

if __name__ == '__main__':
    pytest.main('test_project.py')

+ никто не отменял сабпроцесс;)

Ссылки

http://pytest.org

Автор: Евгений Курочкин