Замена fixtures для тестов или обзор factory-boy
Facrtory-boy — это такая замена fixtures в django, которая позволяет более гибко и удобно генерировать данные для тестов с использование различных стратегий. Можно возвращать либо сохраненные модели, либо просто модели, пока еще не сохраненные, либо просто словарь атрибутов модели, связывать фабрики между собой. Раздолье для творчества. А написана она была Mark Sandstrom и сейчас активно развивается Raphaël Barrois. Идея была позаимствована из аналогичной библиотеки factory-girl для руби.
На factory-boy я набрел случайно, увидел название в комментариях на хабре. Погуглил и нашел factory-boy на гитхабе.
Либо ставим с помощью
либо устанавливаем из исходников:
Каждая фабрика представляет из себя класс, унаследованный от factory.Factory. Рекомендуется размещать фабрики в файле factories.py в директории с вашими тестами.
Создадим фабрики для пользователей (django.contrib.auth.User):
factory_boy поддерживает несколько различных стратегий создания экземпляров: build, create, attributes и stub.
Можно изменить это поведение переопределив свойство default_strategy. Это можно сделать как для конкретной фабрики, так и для всех дочерних фабрик factory.Factory
На мой взгляд — это одна из самых убойных фич factory-boy. Она позволяет присваивать значения одних полей на основе значений других полей.
Если для ленивого атрибута требуются какие-то значительные преобразования, которые не помещаются в лямбда функцию, то можно использовать декоратор для методов:
Это еще одна из убойных фич. Они нужны для того, чтобы создать связи ForeignKey моделей.
Фабрики можно наследовать и переписывать свойства.
factory.Sequence позволяет генерировать последовательности чисел. Этот прие может понадобиться для создания уникальных значений имён, email адресов и т.д.
Последовательности можно комбинировать с ленивыми атрибутами:
или можно определить свой собственные алгоритм генерации последовательностей путем переопределения метода _setup_next_sequence:
Иногда не достаточно переопределить стратегию создания экземпляра и нужно вмешать в этот процесс. Здесь нам поможет переопределение метода _prepare:
Если одна из ваших фабрик имеет поле, которое тоже является фабрикой, то вы можете определить его с помощью SubFactory. Лучше посмотреть пример работы:
Уверен, что использование этой библиотеки спасет кучу времени и облегчит работу, связанную с поддержанием fixtures в актуальном состоянии.
Еще более подробно можно ознакомиться с исходниками на странице: factory-boy
На factory-boy я набрел случайно, увидел название в комментариях на хабре. Погуглил и нашел factory-boy на гитхабе.
Установка
Либо ставим с помощью
easy_install factory_boy
либо устанавливаем из исходников:
python setup.py
Определение фабрик
Каждая фабрика представляет из себя класс, унаследованный от factory.Factory. Рекомендуется размещать фабрики в файле factories.py в директории с вашими тестами.
Создадим фабрики для пользователей (django.contrib.auth.User):
import factory
from models import User
# Фабрика для создания обычного пользователя
class UserFactory(factory.Factory):
FACTORY_FOR = User
first_name = 'John'
last_name = 'Doe'
admin = False
# Фабрика для создания привилегированного пользователя
class AdminFactory(factory.Factory):
FACTORY_FOR = User
first_name = 'Admin'
last_name = 'User'
admin = True
Использование
factory_boy поддерживает несколько различных стратегий создания экземпляров: build, create, attributes и stub.
# Возвращает не сохраненный экземпляр User
user = UserFactory.build()
# Возвращает сохраненный экземпляр User
user = UserFactory.create()
# Возвращает словарь атрибутов, которые могут использоваться
# при создании экземпляра User
attributes = UserFactory.attributes()
# Возвращает объект со всеми определенными атрибутами
stub = UserFactory.stub()
user = UserFactory()
# аналогично вызову
user = UserFactory.create()
Можно изменить это поведение переопределив свойство default_strategy. Это можно сделать как для конкретной фабрики, так и для всех дочерних фабрик factory.Factory
UserFactory.default_strategy = factory.BUILD_STRATEGY
user = UserFactory()
# переписываем стратегию для всех фабрик
factory.Factory.default_strategy = factory.BUILD_STRATEGY
Ленивые атрибуты
На мой взгляд — это одна из самых убойных фич factory-boy. Она позволяет присваивать значения одних полей на основе значений других полей.
class UserFactory(factory.Factory):
first_name = 'Joe'
last_name = 'Blow'
email = factory.LazyAttribute(lambda a: '{0}.{1}@example.com'.format(a.first_name, a.last_name).lower())
UserFactory().email
# => 'joe.blow@example.com'
Если для ленивого атрибута требуются какие-то значительные преобразования, которые не помещаются в лямбда функцию, то можно использовать декоратор для методов:
# Stub фабрики не могут быть ассоциированы с классом
class SumFactory(factory.StubFactory):
lhs = 1
rhs = 1
@lazy_attribute
def sum(a):
result = a.lhs + a.rhs # Or some other fancy calculation
return result
Ассоциации
Это еще одна из убойных фич. Они нужны для того, чтобы создать связи ForeignKey моделей.
from models import Post
class PostFactory(factory.Factory):
FACTORY_FOR = Post
author = factory.LazyAttribute(lambda a: UserFactory())
# Создаем и сохраняем наш экземпляр
post = PostFactory()
post.id == None # => False
post.author.id == None # => False
# Создаем экземпляр Post с сохраненным свойством author
post = PostFactory.build()
post.id == None # => True
post.author.id == None # => False
Наследование
Фабрики можно наследовать и переписывать свойства.
class PostFactory(factory.Factory):
title = 'A title'
class ApprovedPost(PostFactory):
approved = True
approver = factory.LazyAttribute(lambda a: UserFactory())
Последовательности
factory.Sequence позволяет генерировать последовательности чисел. Этот прие может понадобиться для создания уникальных значений имён, email адресов и т.д.
class UserFactory(factory.Factory):
email = factory.Sequence(lambda n: 'person{0}@example.com'.format(n))
UserFactory().email # => 'person0@example.com'
UserFactory().email # => 'person1@example.com'
Последовательности можно комбинировать с ленивыми атрибутами:
class UserFactory(factory.Factory):
name = 'Mark'
email = factory.LazyAttributeSequence(lambda a, n: '{0}+{1}@example.com'.format(a.name, n).lower())
UserFactory().email # => mark+0@example.com
или можно определить свой собственные алгоритм генерации последовательностей путем переопределения метода _setup_next_sequence:
class MyFactory(factory.Factory):
@classmethod
def _setup_next_sequence(cls):
return cls._associated_class.objects.values_list('id').order_by('-id')[0] + 1
Низкоуровневый тюнинг
Иногда не достаточно переопределить стратегию создания экземпляра и нужно вмешать в этот процесс. Здесь нам поможет переопределение метода _prepare:
class UserFactory(factory.Factory):
FACTORY_FOR = User
username = factory.LazyAttributeSequence(lambda a, n: 'username_{0}'.format(n))
first_name = factory.LazyAttributeSequence(lambda a, n: 'first_name_{0}'.format(n))
last_name = factory.LazyAttributeSequence(lambda a, n: 'last_name_{0}'.format(n))
email = factory.LazyAttributeSequence(lambda a, n: 'person{0}@example.com'.format(n))
password = "password"
is_staff = False
is_active = True
is_superuser = False
@classmethod
def _prepare(cls, create, **kwargs):
password = kwargs.pop('password', None)
user = super(UserFactory, cls)._prepare(create, **kwargs)
if password:
user.set_password(password)
if create:
user.save()
return user
Subfactories
Если одна из ваших фабрик имеет поле, которое тоже является фабрикой, то вы можете определить его с помощью SubFactory. Лучше посмотреть пример работы:
class InnerFactory(factory.Factory):
foo = 'foo'
bar = factory.LazyAttribute(lambda o: foo * 2)
class ExternalFactory(factory.Factory):
inner = factory.SubFactory(InnerFactory, foo='bar')
>>> e = ExternalFactory()
>>> e.foo
'bar'
>>> e.bar
'barbar'
>>> e2 : ExternalFactory(inner__bar='baz')
>>> e2.foo
'bar'
>>> e2.bar
'baz'
Заключение
Уверен, что использование этой библиотеки спасет кучу времени и облегчит работу, связанную с поддержанием fixtures в актуальном состоянии.
Еще более подробно можно ознакомиться с исходниками на странице: factory-boy



комментарии (15)