Django Framework

индекс
177,76

Замена fixtures для тестов или обзор factory-boy

Facrtory-boy — это такая замена fixtures в django, которая позволяет более гибко и удобно генерировать данные для тестов с использование различных стратегий. Можно возвращать либо сохраненные модели, либо просто модели, пока еще не сохраненные, либо просто словарь атрибутов модели, связывать фабрики между собой. Раздолье для творчества. А написана она была Mark Sandstrom и сейчас активно развивается Raphaël Barrois. Идея была позаимствована из аналогичной библиотеки factory-girl для руби.



На 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
+14
16 января 2012, 19:41
39

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

+2
toxicmt #
Нашелся таки бой у нашей герл (https://github.com/thoughtbot/factory_girl) ;)
+1
salvator #
У нас уже есть django-any, но оно не настолько управляемое, и иногда подглючивает (по крайне мере, так было года пол назад, когда последний раз юзал его). Радует, что появилась альтернатива. Попробую на днях.
+1
qnub #
django_any сильно проще и если не нужно таких наворотов — то вполне достаточно. оригинальный вроде как подзаброшен — на багрепорт никто не уотрегаировал уже более недели (последний коммит 2.08.2011), но есть форк, за которым вроде бы следят — django-whatever (последний коммит 12.01.2012). он использует тоже пространство имён, хотя имя пакета в PyPi другое.
+2
Prophet #
В django-whatever я часть багов починил, добавил несколько фич и документацию. Присылайте багрепорты, если будут, починим.

Мы уже несколько месяцев используем django-any/whatever в качестве замены фикстур и нас пока устраивает всё. Единственным концептуальным недостатком является то, что из-за случайности данных при генерации моделей тесты могут случайным образом проваливаться. В этом случае надо быть аккуратным при указании тех атрибутов, которые должны быть случайными, а какие нет.
0
qnub #
Да, заметил — мне кажется нужно выставлять поля в дефолт если не всегда, то опционально сделать такую возможность. Я часто ожидают что у модели дефолтное значение у незаполненного поля, а там не пойми что на самом деле. За поддержку пакета — спасибо!
0
Prophet #
Есть такое, пока что экспериментально отдельной функцией:

from django_any.contrib import any_model_with_defaults

poll = any_model_with_defaults(Model)
0
qnub #
Здорово! Спасибо ещё раз!
+1
artifex #
django-whatever не то, чтобы проще, он лаконичней и, на мой вкус, на порядок удобней этого вот «фабричного мальчика». По крайней мере то, что я увидел в factory-boy меня не впечатлило. Удобней как минимум то, что можно писать подобные генераторы in-place и не городить огород с инкапсуляцией фабрик. Если мне нужно использовать генераторы повторно, я выношу их в setUp или в свой TestCase, если тестов нужно дохрена. Ну, то есть, не вижу ничего такого, чтобы прям «ах, вещь, давайте заюзаем».
0
qnub #
Согласен, ничего нужного для меня, чего нет в django-whatever у мальчика я не нашёл, только лишняя писанина. Возможно у меня слишком тривиальные ситуации :)
+2
kmmbvnr #
Лаконичней, то оно лаконичней. Но я как создатель django_any могу сказать, что я в подходе несколько разочаровался =)

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

Тока все руки не доходят.
0
S2nek #
Вот, приятно увидеть трезвый взгляд, а не навязывание своего мнения. Никто не мешает использовать и factory-boy и django-any как раз в зависимости от ситуации.
0
S2nek #
Если под «сильно проще» подразумевается вот такой вариант использования obj = factory.create(FakeDjangoModel, foo='bar'), то factory-boy и его поддерживает.
+1
Prophet #
А какие значения будут у других полей FakeDjangoModel (с foo всё ясно) по-умолчанию, если свою фабрику не создавать?
0
S2nek #
Нет, сейчас он не генерирует случайные данные.
+1
S2nek #
Удивлен, что топик об одной библиотеке превратился в обсуждение другой библиотеки.

А говорить, что factory-boy не нужен, потому-что не имеет каких-то возможностей, которые реализованы в django-any (с позиции пользователя django-any), ну это неправильно.

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