В статье рассматривается несколько ключевых технологий на языке Python, которые мы применяем в работе, чтобы обеспечить надежность разрабатываемых Python-приложений: unittest, pytest, unittest.mock, Freeze Gun, Webtest, Factory Boy, tox, retrying, Cosmic Ray, BitBucket Pipelines.

Введение

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

Пирамида надежного  программирования

Согласно пирамиде надежного программирования ключевыми являются вопросы профессиональной культуры, т. к. без профессиональной культуры все остальные аспекты (менеджмент, методы, технологии) не имеют смысла. Тем не менее, программная реализация в конечном счете выражается в использовании конкретных технологий (библиотек, сервисов). Рассмотрим, какие технологии помогают нам в Design and Test Lab разрабатывать надежные облачные приложения на языке Питон.

unittest (тестирование)

unittest — фреймворк для разработки и запуска модульных тестов.

Рассмотрим пример из официальной документации языка Python. Данный пример содержит три теста:

  1. test_upper — проверяет, что строка корректно преобразуется во все заглавные буквы.
  2. test_isupper — проверяет, что метод isupper корректно возвращает значение
  3. test_split — проверяет, что строка корректно разбилась по пробелу, а тажке тот факт, что аргументом метода split не может быть целое число.
import unittest

class TestStringMethods(unittest.TestCase):

    def test_upper(self):
        # Проверяем, что строка преобразуется во все заглавные.
        self.assertEqual('foo'.upper(), 'FOO')

    def test_isupper(self):
        # Проверяем метод isupper()
        self.assertTrue('FOO'.isupper())
        self.assertFalse('Foo'.isupper())

    def test_split(self):
        s = 'hello world'
        # Проверяем деление строки по пробелу.
        self.assertEqual(s.split(), ['hello', 'world'])
        # Проверяем, что аргументом метода split не может быть целое число.
        with self.assertRaises(TypeError):
            s.split(2)

Официальная документация: https://docs.python.org/3/library/unittest.html

pytest (тестирование)

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

Пример из официальной документации (простейший тест из четырех строк):

def func(x):
    return x + 1

def test_answer():
    assert func(3) == 5

pytest использует стандартное ключевое слово assert для проверок. В этом тесте мы проверяем, что значение функции равно 5. Разумеется, этот тест не пройдет.

Официальная документация: https://docs.pytest.org/en/latest/

unittest.mock (тестопригодность)

unittest.mock — библиотека для повышения тестопригодности модулей. Библиотека позволяет подменить части объекта тестирования специальными управляемыми объектами (моками). Это позволяет повысить управляемость и наблюдаемость объекта тестирования.

Часто бывает, что сложно протестировать систему, потому что она зависит от внешних модулей или аппаратуры. unittest.mock как раз и решает эту проблему.

Надежно!

Позаботься о тестопригодности системы еще на этапе ее проектирования!

Хорошими кандидатами для подмены являются:

  • аппаратные модули, например, датчик времени, работа с файловой системой;
  • модули с непредсказуемым поведением: генератор случайных чисел, генератор уникальных идентификаторов;
  • сетевые зависимости, например, вебсервисы.

Пример из официально документации:

>>> from unittest.mock import MagicMock

>>> thing = ProductionClass()
>>> thing.method = MagicMock(return_value=3)
>>> thing.method(3, 4, 5, key='value')

3

>>> thing.method.assert_called_with(3, 4, 5, key='value')

Здесь у экземпляра класса ProductionClass мы подменяем реализацию метода method специальным мок-объектом. Мы задаем, что он должен возвращать всегда значение 3 (управляемость). В конце мы проверяем, что метод был вызван с аргументами 3, 4, 5, key="value" (наблюдаемость).

Всегда, когда сложно тестировать — это проблемы с тестопригодностью. unittest.mock — отличный способ существенно улучшить управляемость и наблюдаемость объекта тестирования.

Официальная документация: https://docs.python.org/3/library/unittest.mock.html

FreezeGun (тестопригодность)

FreezeGun — библиотека для повышения тестопригодности модулей, которые зависят от времени (datetime).

Эту библиотеку можно считать частным случаем библиотеки unittest.mock. Специализация Freezegun — управлять временем. Например, нужно проверить, что у пользователя есть подписка на сервис только до конца октября месяца. Нужно проверить несколько случаев:

  • 31 октября 2019 года, 23:59:59 — доступ еще есть;
  • 1 ноября 2019 года, 00:00:00 — доступа уже нет.

Freezegun как раз и позволяет управлять временем в момент запуска тестов.

Пример из официальной документации:

from freezegun import freeze_time
import datetime
import unittest

@freeze_time("1955-11-12")
class MyTests(unittest.TestCase):
    def test_the_class(self):
        assert datetime.datetime.now() == datetime.datetime(1955, 11, 12)

Официальная документация: https://github.com/spulec/freezegun

WebTest (тестирование)

WebTest — библиотека, которая позволяет тестировать веб-приложения через WSGI—интерфейс. Популярные веб-фреймворки поддерживающие этот интефрейс:

  • Django
  • Flask
  • webapp2 (Google App Engine)
  • mod_wsgi
  • Pylons
  • Pyramid
  • Tornado
  • uWSGI
  • и многие другие.

WebTest позволяет сформировать HTTP-запрос, отправить его объекту тестирования, а затем проанализировать HTTP-ответ.

Рассмотрим простейшее веб-приложение в качестве объекта тестирования (примеры из официальной документации):

def application(environ, start_response):
    headers = [('Content-Type', 'text/html; charset=utf8'),
        ('Content-Length', str(len(body)))]
    start_response('200 OK', headers)
    return [body]

Отправляем запрос и анализируем результат:

from webtest import TestApp
app = TestApp(application)
resp = app.get('/')

assert resp.status == '200 OK'
assert resp.status_int == 200
assert resp.content_type == 'text/html'
assert resp.content_length > 0
resp.mustcontain('<html>')
assert 'form' in resp

WebTest позволяет отправлять и анализировать JSON-запросы:

resp = app.post_json('/resource/', dict(id=1, value='value'))

assert resp.json == {'id': 1, 'value': 'value'}

WebTest может быть легко комбинирован с библиотеками unittest, pytest, unittest.mock, FreezeGun и прочими.

Официальная документация: https://docs.pylonsproject.org/projects/webtest/en/latest/

Factory Boy (тестопригодность)

Factory Boy — библиотека, реализущая шаблон Object Mother, позволящий легко подготавливать тестовые данные для тестов.

Одно из сопротивлений автоматическому тестированию — это то, что нужно подготавливать тестовые данные: подбирать набор тех предусловий и объектов, а также их состояний для тестирования. Factory Boy позволяет легко генерировать тестовые объекты на основе примеров, а также легко менять их атрибуты.

Рассмотрим пример из официальной документации.

Сначала мы описываем два объекта предметной области: Account, Profile:

class Account(object):
    def __init__(self, username, email, date_joined):
        self.username = username
        self.email = email
        self.date_joined = date_joined

    def __str__(self):
        return '%s (%s)' % (self.username, self.email)


class Profile(object):

    GENDER_MALE = 'm'
    GENDER_FEMALE = 'f'
    GENDER_UNKNOWN = 'u'  # If the user refused to give it

    def __init__(self, account, gender, firstname, lastname, planet='Earth'):
        self.account = account
        self.gender = gender
        self.firstname = firstname
        self.lastname = lastname
        self.planet = planet

    def __unicode__(self):
        return u'%s %s (%s)' % (
            unicode(self.firstname),
            unicode(self.lastname),
            unicode(self.account.username),
        )

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

import factory
import random

from . import objects

class AccountFactory(factory.Factory):
    class Meta:
        model = objects.Account

    username = factory.Sequence(lambda n: 'john%s' % n)
    email = factory.LazyAttribute(lambda o: '%s@example.org' % o.username)
    date_joined = factory.LazyFunction(datetime.datetime.now)


class ProfileFactory(factory.Factory):
    class Meta:
        model = objects.Profile

    account = factory.SubFactory(AccountFactory)
    gender = factory.Iterator([objects.Profile.GENDER_MALE, objects.Profile.GENDER_FEMALE])
    firstname = u'John'
    lastname = u'Doe'

class FemaleProfileFactory(ProfileFactory):
    gender = objects.Profile.GENDER_FEMALE
    firstname = u'Jane'
    account__username = factory.Sequence(lambda n: 'jane%s' % n)

Обратите внимание, что здесь создана специальная фабрика FemaleProfileFactory, которая создает профили пользователей с женским полом. Это полезно, если для тестирования нужно создавать часто объекты в специфическом состоянии.

Теперь можно использовать все эти фабрики в тесте:

import unittest

from . import business_logic
from . import factories
from . import objects


class MyTestCase(unittest.TestCase):

    def test_get_profile_stats(self):
        profiles = []

        profiles.extend(factories.ProfileFactory.create_batch(4))
        profiles.extend(factories.FemaleProfileFactory.create_batch(2))
        profiles.extend(factories.ProfileFactory.create_batch(2, planet="Tatooine"))

        stats = business_logic.profile_stats(profiles)
        self.assertEqual({'Earth': 6, 'Mars': 2}, stats.planets)
        self.assertLess(stats.genders[objects.Profile.GENDER_FEMALE], 2)

Обратите внимание с какой легкостью (всего три строчки кода) здесь созданы 8 разных профилей пользователя.

Официальная документация: https://factoryboy.readthedocs.io/en/latest/index.html

tox (тестирование)

tox — библиотека, которая позволяет стандартизировать и автоматизировать тестирование для Питон-приложений. tox работает на основе virtualenv, библиотека сама конфигурирует все необъодимые зависимости для сборки объекта тестирования, а также зависимости для запуска тестов. Таким образом, запуск тестов на новом компьютере заключается лишь в запуске одной единственной команды tox в консоли.

Учитываея свои особенности, tox очень хорошо подходит для систем интеграционного тестирования, т. к. не требует никаких дополнительных настроек.

Официальная документация: https://tox.readthedocs.io/en/latest/

retrying (бронирование важнейших узлов)

retrying — библиотека, которая позволяет перехватывать и обрабатывать отказы любых функций и методов. Реализует шаблон “декоратор”.

Часто разные зависимости возвращают исключение. Например, при обращении к файлу он еще не был создан. Или при подключении к серверу, вернулась ошбика time-out. retrying позволяет автоматически повторить запрос, тем самым повысить отказоустойчивость системы.

Пример из официальной документации:

@retry
def never_give_up_never_surrender():
    print "Эта функция будет вызываться до тех пор, пока не будет исключений."

Можно повторять вызов только для специфических исключений:

def retry_if_io_error(exception):
    """Return True if we should retry (in this case when it's an IOError), False otherwise"""
    return isinstance(exception, IOError)

@retry(retry_on_exception=retry_if_io_error)
def might_io_error():
    print "Retry forever with no wait if an IOError occurs, raise any other errors"

@retry(retry_on_exception=retry_if_io_error, wrap_exception=True)
def only_raise_retry_error_when_not_io_error():
    print "Retry forever with no wait if an IOError occurs, raise any other errors wrapped in RetryError"

Можно повторять вызов до тех пор, пока функция или метод не вернут не-None значание:

def retry_if_result_none(result):
    """Return True if we should retry (in this case when result is None), False otherwise"""
    return result is None

@retry(retry_on_result=retry_if_result_none)
def might_return_none():
    print "Retry forever ignoring Exceptions with no wait if return value is None"

Официальная документация: https://github.com/rholder/retrying

Cosmic Ray (качество тестов)

Cosmic Ray — инструмент моделирования неисправностей в объекте тестирования (мутационное тестирование).

Cosmic Ray вносит неисправности — небольшие изменения в объект тестирования и проверяет, обнаруживаются ли эти неиспрановсти текущим набором тестов. Тем самым Cosmic Ray может подсказать, какие неисправности не проверяются тестами.

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

Примеры работы этого инструмента можно посмотреть в лекции Владимира Обризана, к. т. н, директор Design and Test Lab:


Официальная документация: https://cosmic-ray.readthedocs.io/en/latest/

BitBucket Pipelines (тестирование)

BitBucket Pipelines — это сервис непрерывной интеграции и поставки приложений.

Сервис автоматически запускает тесты на на каждое изменение в продукте. В этом нам помогает утилита tox. Это определяется в в специальном файле bitbucket-pipelines.yaml:

image: official/python3

pipelines:
  default:
    - step:
        script:
          - tox

BitBucket Pipelines выполняет эти скрипты для каждого изменения в репозитории:

Результат запуска BitBucket Pipelines.

Для каждого запуска можно посмотреть результат запуска. Здесь видно, что для коммита 415faef9 успелно выполнились 205 тестов:

Результат запуска тестов.

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

Ненадежно!

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

Официальная докуменнтация: https://bitbucket.org/product/features/pipelines

Выводы

Инженеры Design and Test Lab не пишут без тестов. Упомянутые технологии существенно упрощают написание тестов и поддержку продукта в исправном состоянии.

Различные библиотеки можно комбинировать между собой. Например, для интеграционного тестирования бекенда можно применить tox, WebTest, unittest.mock, BitBucket Pipelines. В большинестве случаев одной технологии недостаточно, чтобы обеспечить надежную работу приложения.

Контрольные вопросы

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

  1. В чем отличие библиотек unittest.mock и Freeze Gun?
  2. В чем отличие pytest и WebTest?
  3. Вам нужно подготавливать тестовые данные в различных состояниях. Какая библиотека решает эту задачу?
  4. Какой сервис позволяет автоматически запускать тесты и проверять исправность продукта?
  5. Почему репозиторий нужно держать всегда в исправном состоянии?
  6. Как проверить качество написанного теста?
  7. Вам нужно изолировать модуль от внешней зависимости, например, перехватить запрос к серверу. Какой библиотекой решить эту задачу?
  8. Зависимость изредка выбрасывает исключение, чем прерывает нормальную работу приложения.. Какой библиотекой решить эту проблему?
  9. Можно ли комбинировать упомянутые технологии между собой? Приведите пример.
  10. Нужно протестировать API веб-сервиса. Какими технологиями вы будете это делать?