Skip to content

Latest commit

 

History

History
492 lines (304 loc) · 15.2 KB

orms.rst

File metadata and controls

492 lines (304 loc) · 15.2 KB

Using factory_boy with ORMs

factory

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

Django

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 DjangoModelFactory subclass

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

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

This class provides the following features:

  • The ~factory.FactoryOptions.model attribute also supports the 'app.Model' syntax
  • ~factory.Factory.create() uses Model.objects.create() <django.db.models.query.QuerySet.create>
  • When using ~factory.RelatedFactory or ~factory.PostGeneration attributes, the base object will be saved <django.db.models.Model.save> once all post-generation hooks have run.

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 ~factory.SubFactory pointing to another model: Django refused to set a ~djang.db.models.ForeignKey to an unsaved ~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 ~DjangoModelFactory supports extra parameters:

database

2.5.0

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

django_get_or_create

2.4.0

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

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

    username = 'john'
>>> 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 django.db.models.FileField

__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 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 django.db.models.ImageField

__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 FileField field, this will disable field generation:

Note

Just as Django's django.db.models.ImageField requires the Python Imaging Library, this 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 factories <DjangoModelFactory>, which would create both using ~factory.RelatedFactory.

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

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

factory.mogo

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

Mogo is a wrapper around the pymongo library for MongoDB.

Dedicated class for Mogo models.

This class provides the following features:

  • ~factory.Factory.build() calls a model's new() method
  • ~factory.Factory.create() builds an instance through new() then saves it.

MongoEngine

factory.mongoengine

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

mongoengine is a wrapper around the pymongo library for MongoDB.

Dedicated class for MongoEngine models.

This class provides the following features:

  • ~factory.Factory.build() calls a model's __init__ method
  • ~factory.Factory.create() builds an instance through __init__ then saves it.

Note

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

This feature makes it possible to use ~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

factory.alchemy

Factoy_boy also supports SQLAlchemy models through the SQLAlchemyModelFactory class.

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

Dedicated class for SQLAlchemy models.

This class provides the following features:

  • ~factory.Factory.create() uses sqlalchemy.orm.session.Session.add

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

sqlalchemy_session

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

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 ~sqlalchemy.orm.session.Session.flush
  • 'commit': perform a session ~sqlalchemy.orm.session.Session.commit

The default value is None.

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

force_flush

Force a session flush() at the end of ~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 sqlalchemy.orm.scoping.scoped_session:

  • The test runner configures some project-wide ~sqlalchemy.orm.scoping.scoped_session
  • Each ~SQLAlchemyModelFactory subclass uses this ~sqlalchemy.orm.scoping.scoped_session as its ~SQLAlchemyOptions.sqlalchemy_session
  • The ~unittest.TestCase.tearDown method of tests calls Session.remove <sqlalchemy.orm.scoping.scoped_session.remove> to reset the session.

Note

See the excellent SQLAlchemy guide on scoped_session <sqlalchemy:unitofwork_contextual> for details of ~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 ~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:

  • A global (test-only?) file holds the ~sqlalchemy.orm.scoping.scoped_session:
# 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)
  • The test runner configures the ~sqlalchemy.orm.scoping.scoped_session when it starts:
# 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
  • test cases <unittest.TestCase> use this scoped_session, and clear it after each test (for isolation):
# 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()