Permalink
493 lines (304 sloc) 15.2 KB

Using factory_boy with ORMs

.. currentmodule:: factory


factory_boy provides custom :class:`Factory` subclasses for various ORMs, adding dedicated features.

Django

.. currentmodule:: factory.django


The first versions of factory_boy were designed specifically for Django, but the library has now evolved to be framework-independent.

Most features should thus feel quite familiar to Django users.

The :class:`DjangoModelFactory` subclass

All factories for a Django :class:`~django.db.models.Model` should use the :class:`DjangoModelFactory` base class.

Dedicated class for Django :class:`~django.db.models.Model` factories.

This class provides the following features:

Note

With Django versions 1.8.0 to 1.8.3, it was no longer possible to call .build() on a factory if this factory used a :class:`~factory.SubFactory` pointing to another model: Django refused to set a :class:`~djang.db.models.ForeignKey` to an unsaved :class:`~django.db.models.Model` instance.

See https://code.djangoproject.com/ticket/10811 and https://code.djangoproject.com/ticket/25160 for details.

The class Meta on a :class:`~DjangoModelFactory` supports extra parameters:

.. attribute:: database

    .. versionadded:: 2.5.0

    All queries to the related model will be routed to the given database.
    It defaults to ``'default'``.

.. attribute:: django_get_or_create

    .. versionadded:: 2.4.0

    Fields whose name are passed in this list will be used to perform a
    :meth:`Model.objects.get_or_create() <django.db.models.query.QuerySet.get_or_create>`
    instead of the usual :meth:`Model.objects.create() <django.db.models.query.QuerySet.create>`:

    .. code-block:: python

        class UserFactory(factory.django.DjangoModelFactory):
            class Meta:
                model = 'myapp.User'  # Equivalent to ``model = myapp.models.User``
                django_get_or_create = ('username',)

            username = 'john'

    .. code-block:: pycon

        >>> User.objects.all()
        []
        >>> UserFactory()                   # Creates a new user
        <User: john>
        >>> User.objects.all()
        [<User: john>]

        >>> UserFactory()                   # Fetches the existing user
        <User: john>
        >>> User.objects.all()              # No new user!
        [<User: john>]

        >>> UserFactory(username='jack')    # Creates another user
        <User: jack>
        >>> User.objects.all()
        [<User: john>, <User: jack>]

Extra fields

Custom declarations for :class:`django.db.models.FileField`

.. method:: __init__(self, from_path='', from_file='', data=b'', filename='example.dat')

    :param str from_path: Use data from the file located at ``from_path``,
                          and keep its filename
    :param file from_file: Use the contents of the provided file object; use its filename
                           if available, unless ``filename`` is also provided.
    :param func from_func: Use function that returns a file object
    :param bytes data: Use the provided bytes as file contents
    :param str filename: The filename for the FileField

Note

If the value None was passed for the :class:`FileField` field, this will disable field generation:

class MyFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = models.MyModel

    the_file = factory.django.FileField(filename='the_file.dat')
>>> MyFactory(the_file__data=b'uhuh').the_file.read()
b'uhuh'
>>> MyFactory(the_file=None).the_file
None

Custom declarations for :class:`django.db.models.ImageField`

.. method:: __init__(self, from_path='', from_file='', filename='example.jpg', width=100, height=100, color='green', format='JPEG')

    :param str from_path: Use data from the file located at ``from_path``,
                          and keep its filename
    :param file from_file: Use the contents of the provided file object; use its filename
                           if available
    :param func from_func: Use function that returns a file object
    :param str filename: The filename for the ImageField
    :param int width: The width of the generated image (default: ``100``)
    :param int height: The height of the generated image (default: ``100``)
    :param str color: The color of the generated image (default: ``'green'``)
    :param str format: The image format (as supported by PIL) (default: ``'JPEG'``)

Note

If the value None was passed for the :class:`FileField` field, this will disable field generation:

Note

Just as Django's :class:`django.db.models.ImageField` requires the Python Imaging Library, this :class:`ImageField` requires it too.

class MyFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = models.MyModel

    the_image = factory.django.ImageField(color='blue')
>>> MyFactory(the_image__width=42).the_image.width
42
>>> MyFactory(the_image=None).the_image
None

Disabling signals

Signals are often used to plug some custom code into external components code; for instance to create Profile objects on-the-fly when a new User object is saved.

This may interfere with finely tuned :class:`factories <DjangoModelFactory>`, which would create both using :class:`~factory.RelatedFactory`.

To work around this problem, use the :meth:`mute_signals()` decorator/context manager:

.. method:: mute_signals(signal1, ...)

    Disable the list of selected signals when calling the factory, and reactivate them upon leaving.

# foo/factories.py

import factory
import factory.django

from . import models
from . import signals

@factory.django.mute_signals(signals.pre_save, signals.post_save)
class FooFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = models.Foo

    # ...

def make_chain():
    with factory.django.mute_signals(signals.pre_save, signals.post_save):
        # pre_save/post_save won't be called here.
        return SomeFactory(), SomeOtherFactory()

Mogo

.. currentmodule:: factory.mogo

factory_boy supports Mogo-style models, through the :class:`MogoFactory` class.

Mogo is a wrapper around the pymongo library for MongoDB.

Dedicated class for Mogo models.

This class provides the following features:

MongoEngine

.. currentmodule:: factory.mongoengine

factory_boy supports MongoEngine-style models, through the :class:`MongoEngineFactory` class.

mongoengine is a wrapper around the pymongo library for MongoDB.

Dedicated class for MongoEngine models.

This class provides the following features:

Note

If the :attr:`associated class <factory.FactoryOptions.model` is a :class:`mongoengine.EmbeddedDocument`, the :meth:`~MongoEngineFactory.create` function won't "save" it, since this wouldn't make sense.

This feature makes it possible to use :class:`~factory.SubFactory` to create embedded document.

A minimalist example:

import mongoengine

class Address(mongoengine.EmbeddedDocument):
    street = mongoengine.StringField()

class Person(mongoengine.Document):
    name = mongoengine.StringField()
    address = mongoengine.EmbeddedDocumentField(Address)

import factory

class AddressFactory(factory.mongoengine.MongoEngineFactory):
    class Meta:
        model = Address

    street = factory.Sequence(lambda n: 'street%d' % n)

class PersonFactory(factory.mongoengine.MongoEngineFactory):
    class Meta:
        model = Person

    name = factory.Sequence(lambda n: 'name%d' % n)
    address = factory.SubFactory(AddressFactory)

SQLAlchemy

.. currentmodule:: factory.alchemy


Factoy_boy also supports SQLAlchemy models through the :class:`SQLAlchemyModelFactory` class.

To work, this class needs an SQLAlchemy session object affected to the :attr:`Meta.sqlalchemy_session <SQLAlchemyOptions.sqlalchemy_session>` attribute.

Dedicated class for SQLAlchemy models.

This class provides the following features:

In addition to the usual parameters available in :class:`class Meta <factory.base.FactoryOptions>`, a :class:`SQLAlchemyModelFactory` also supports the following settings:

.. attribute:: sqlalchemy_session

    SQLAlchemy session to use to communicate with the database when creating
    an object through this :class:`SQLAlchemyModelFactory`.

.. attribute:: sqlalchemy_session_persistence

    Control the action taken by sqlalchemy session at the end of a create call.

    Valid values are:

    * ``None``: do nothing
    * ``'flush'``: perform a session :meth:`~sqlalchemy.orm.session.Session.flush`
    * ``'commit'``: perform a session :meth:`~sqlalchemy.orm.session.Session.commit`

    The default value is ``None``.

    If ``force_flush`` is set to ``True``, it overrides this option.

.. attribute:: force_flush

    Force a session ``flush()`` at the end of :func:`~factory.alchemy.SQLAlchemyModelFactory._create()`.

    .. note::

        This option is deprecated. Use ``sqlalchemy_session_persistence`` instead.

A (very) simple example:

from sqlalchemy import Column, Integer, Unicode, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import scoped_session, sessionmaker

engine = create_engine('sqlite://')
session = scoped_session(sessionmaker(bind=engine))
Base = declarative_base()


class User(Base):
    """ A SQLAlchemy simple model class who represents a user """
    __tablename__ = 'UserTable'

    id = Column(Integer(), primary_key=True)
    name = Column(Unicode(20))

Base.metadata.create_all(engine)

import factory

class UserFactory(factory.alchemy.SQLAlchemyModelFactory):
    class Meta:
        model = User
        sqlalchemy_session = session   # the SQLAlchemy session object

    id = factory.Sequence(lambda n: n)
    name = factory.Sequence(lambda n: u'User %d' % n)
>>> session.query(User).all()
[]
>>> UserFactory()
<User: User 1>
>>> session.query(User).all()
[<User: User 1>]

Managing sessions

Since SQLAlchemy is a general purpose library, there is no "global" session management system.

The most common pattern when working with unit tests and factory_boy is to use SQLAlchemy's :class:`sqlalchemy.orm.scoping.scoped_session`:

Note

See the excellent :ref:`SQLAlchemy guide on scoped_session <sqlalchemy:unitofwork_contextual>` for details of :class:`~sqlalchemy.orm.scoping.scoped_session`'s usage.

The basic idea is that declarative parts of the code (including factories) need a simple way to access the "current session", but that session will only be created and configured at a later point.

The :class:`~sqlalchemy.orm.scoping.scoped_session` handles this, by virtue of only creating the session when a query is sent to the database.

Here is an example layout:

# myprojet/test/common.py

from sqlalchemy import orm
Session = orm.scoped_session(orm.sessionmaker())
  • All factory access it:
# myproject/factories.py

import factory
import factory.alchemy

from . import models
from .test import common

class UserFactory(factory.alchemy.SQLAlchemyModelFactory):
    class Meta:
        model = models.User

        # Use the not-so-global scoped_session
        # Warning: DO NOT USE common.Session()!
        sqlalchemy_session = common.Session

    name = factory.Sequence(lambda n: "User %d" % n)
# myproject/test/runtests.py

import sqlalchemy

from . import common

def runtests():
    engine = sqlalchemy.create_engine('sqlite://')

    # It's a scoped_session, and now is the time to configure it.
    common.Session.configure(bind=engine)

    run_the_tests
# myproject/test/test_stuff.py

import unittest

from . import common

class MyTest(unittest.TestCase):

    def setUp(self):
        # Prepare a new, clean session
        self.session = common.Session()

    def test_something(self):
        u = factories.UserFactory()
        self.assertEqual([u], self.session.query(User).all())

    def tearDown(self):
        # Rollback the session => no changes to the database
        self.session.rollback()
        # Remove it, so that the next test gets a new Session()
        common.Session.remove()