klen.github.io

in Blog

Mixer -- удобная генерация данных для тестирования

Graphite-beacon — простая система оповещений для Graphite Ctrl→
←Ctrl Ускорение python тестов. Конкурентный запуск.


Mixer — это удобная библиотека для генерации данных для тестов. Из коробки обладает поддержкой Django ORM, SQLAlchemy ORM, Mongoengine ODM и простым синтаксисом для интеграции любых бэкэндов. Работает в версиях python 2-х и 3-х версий.

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

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

Mixer предлагает второй способ. Вы получаете данные непосредственно в тестах, в любом необходимом количестве и не зависите от изменений в их структуре.

Пример работы

Несколько примеров для Django как для наиболее распространненного сейчас python-фреймворка.

Создадим стандартного пользователя используя django.contrib.auth.models.User.

from mixer.backend.django import mixer
from django.contrib.auth.models import User

user = mixer.blend(User)

Мы только что создали и сохранили в тестовой базе пользователя, со всеми необходимыми полями. Разумеется Mixer правильно обработает отношения и при необходимости автоматически создаст и объекты зависимых моделей. Вам нет необходимости настраивать параметры создавамого объекта, вы просто указываете модель и остальное Mixer делает самостоятельно.

По-умолчанию Mixer старается генерировать данные не только учитывая тип полей, но и анализируя их названия. Например при проверке примеров библиотека сгенерировала пользователя с логином mixcookie1957, адресом электронной почты: limb_candy@wordpress.br и именем: Robinson Wolverhampton. Но такое поведение разумеется можно отключить и получать абсолютно случайные данные.

Если вам нужно сгенерировать несколько объектов, имеется короткий синтаксис. Создадим еще 5 пользователей.

users = mixer.cycle(5).blend(User)

Зачастую возникает неоходимость предопределить некоторые значения генерируемого объекта. Синтаксис Mixer прост и интуитивно понятен. Создадим пользователя с логином testuser и группой admin.

from django.contrib.auth.models import User, Group

group = mixer.blend(Group, name='admin')
user = mixer.blend(User, username='testuser', groups=[group])

Но Mixer позволяет сделать это еще быстрее. Вышеприведенный код генерации можно заменить одной строчкой:

user = mixer.blend(User, username='testuser', groups__name='admin')

Синтаксис знаком пользователям Django, "__" позволяет указывать в фильтрации запросов, поля зависимых объектов. Так и в Mixer разделитель "__" позволяет предзадавать поля зависимых объектов не заботясь об их генерации. Использование может быть очень гибким.

messages = mixer.cycle(10).blend(
    Message,
    author__name='Duck Nukem',
    author__age='100',
    author__clan__title='crazy beaches',
)

По-умолчанию Mixer пропускает поля имеющие значение по умолчанию или позволяющие None значение. Вы всегда можете предзадать их вручную как в вышеприведенном примере или позволить Mixer сделать это за вас используя волшебный атрибут mixer.random.

class Product(models.Model):
    title = models.CharField(max_length=100)
    price = models.DecimalField(default=0)
    type = models.IntegerField(
        choices=((1, 'food'), (2, 'drinks')),
        null=True
    )

# Здесь price=0 и type=None
mixer.blend(Product)

# А здесь price это какой то decimal, а type равен `food` или `drinks`
mixer.blend(Product, price=mixer.random, type=mixer.random)

В качестве предзаданных данных Mixer поддерживает также функции и генераторы.

# Пример с функцией

def get_super():
    return "super"

mixer.blend(Superman, what=get_super)


# Пример с генератором

gen = (v for v in [10, 20, 30, 40, 50])
products = mixer.cycle(5).blend(Product, price=gen)

# Еще один
authors = mixer.cycle(10).blend(Author)
books = mixer.cycle(5).blend(Book, author=(a for a in authors))

Синтаксис для работы Flask, SQLAlchemy, Mongoengine схож и в данной статье рассматриваться не будет. Импортируете необходимый бэкэнд, указываете модель и получаете данные.

Продвинутое использование

Указание моделей через путь

Вам необязательно импортировать модели для генерации тестовых данных. Mixer способен сделать это за вас. Для Django достаточно указать строку вида: <имя_приложения>.<имя_модели>. Для других бэкэндов придется указать python путь для импорта. Пример из начала статьи можно изменить так:

user = mixer.blend('auth.user')

Другие волшебные атрибуты:

mixer.fake

Поведение mixer.fake похоже на mixer.random, но в отличии от последнего, генерирующего случайные данные, данный атрибут генерирует данные «фейковые данные» анализируя имя поля. Например для строкового поля login это будет строка имитирующая логин пользователя, а не просто случайный набор символов.

test = mixer.blend('app.message', content=mixer.fake)

Note

И mixer.random и mixer.fake позволяют принудительно указать тип генерируемого значения: mixer.random(int), mixer.fake(models.CharField)

mixer.select

mixer.select похож на mixer.fake и mixer.random, но работает с уже существующими данными. В приведенном ниже примере пользователю присваивается одна из существующих в базе данных групп.

user = mixer.blend('auth.user', groups=mixer.select)

mixer.select может принимать параметры фильтрации, например для Django:

user = mixer.blend('auth.user', groups=mixer.select(active=True))

mixer.mix

mixer.mix это волшебный атрибут указывающий на будущее сгенерированное значение. Например создадим пользователя с одинаковыми username и last_name.

user = mixer.blend('auth.user', username=mixer.mix.first_name)

Как мы помним mixer.mix указывает на объект который будет сгенерирован. Поэтому mixer.mix.first_name будет указывать на поле этого объекта и цель достигнута.

Использование mixer.mix может быть и более интересным.

ship = mixer.blend(
    Ship,
    captain__country = mixer.mix.region.country,
    title=mixer.mix.captain.nick
)

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

def get_username(user):
    return user.first_name.lower() + user.last_name.lower()

user = mixer.blend(
    username = mixer.mix(get_username)
)

mixer.sequence

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

Например несколько пользователей с логинами вида: 'test0', 'test1' и тд.

mixer.cycle(10).blend('auth.user', username=mixer.sequence(
    lambda c: 'test' + c
))

В функцию будет приходить счетчик итераций. Вышеприведенная операция встречается довольно часто, поэтому mixer.sequence поддерживает короткий синтаксис. При передачи строки он трансформирует ее в функцию: lambda c: value.format(c).

Результат кода аналогичен предыдущему.

mixer.cycle(10).blend('auth.user', username=mixer.sequence('test{0}'))

Преимущества по сравнению с другими библиотеками

Mixer поддерживает python второй и третьей версии.

Библиотека обладает интеграцией с Django ORM, SQLAlchemy ORM, Mongoengine ODM и легко расширяется для других бэкэндов. Независимо от проекта вы получаете единый интерфейс для генерации данных.

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

Mixer интуитивно понятен, гибок в настройке и прост.

Надеюсь с данным модулем генерация данных станет для вас легче и приятнее.

Graphite-beacon — простая система оповещений для Graphite Ctrl→
←Ctrl Ускорение python тестов. Конкурентный запуск.
alt