django factory boy factory with OneToOne relationship and related field
Question:
I am using Factory Boy to create test factories for my django app. The model I am having an issue with is a very basic Account model which has a OneToOne relation to the django User auth model (using django < 1.5):
# models.py
from django.contrib.auth.models import User
from django.db import models
class Account(models.Model):
user = models.OneToOneField(User)
currency = models.CharField(max_length=3, default='USD')
balance = models.CharField(max_length="5", default='0.00')
Here are my factories:
# factories.py
from django.db.models.signals import post_save
from django.contrib.auth.models import User
import factory
from models import Account
class AccountFactory(factory.django.DjangoModelFactory):
FACTORY_FOR = Account
user = factory.SubFactory('app.factories.UserFactory')
currency = 'USD'
balance = '50.00'
class UserFactory(factory.django.DjangoModelFactory):
FACTORY_FOR = User
username = 'bob'
account = factory.RelatedFactory(AccountFactory)
So I am expecting the factory boy to create a related UserFactory whenever AccountFactory is invoked:
# tests.py
from django.test import TestCase
from factories import AccountFactory
class AccountTest(TestCase):
def setUp(self):
self.factory = AccountFactory()
def test_factory_boy(self):
print self.factory.id
When running the test however, it looks like multiple User models are being create, and I am seeing an integriy error:
IntegrityError: column username is not unique
The documentation does mention watching out for loops when dealing with circular imports, but I am not sure whether that is whats going on, nor how I would remedy it. docs
If anyone familiar with Factory Boy could chime in or provide some insight as to what may be causing this integrity error it would be much appreciated!
Answers:
I believe this is because you have a circular reference in your factory definitions. Try removing the line account = factory.RelatedFactory(AccountFactory)
from the UserFactory
definition. If you are always going to invoke the account creation through AccountFactory, then you shouldn’t need this line.
Also, you may consider attaching a sequence to the name field, so that if you ever do need more than one account, it’ll generate them automatically.
Change: username = "bob"
to username = factory.Sequence(lambda n : "bob {}".format(n))
and your users will be named “bob 1”, “bob 2”, etc.
To pass result of calling UserFactory
to AccountFactory
you should use factory_related_name
(docs)
Code above works next way:
AccountFactory
for instantiating needs SubFactory(UserFactory)
.
UserFactory
instantiates User.
UserFactory
after instantiating calls RelatedFactory(AccountFactory)
- Recursion,.. that is broken due to unique username constraint (you probably want to generate usernames via
FuzzyText
or Sequence
)
So you need write UserFactory
like this:
class UserFactory(factory.django.DjangoModelFactory):
account = factory.RelatedFactory(AccountFactory, factory_related_name='user')
username = factory.Sequence(lambda a: 'email%[email protected]' % a)
# rest of code
But you can still experience issues with already written tests. Imagine you have in tests places like next:
user = UserFactory()
account = Account(user=user)
Then adding RelatedFactory
will break tests. If you haven’t lots of tests and contributors in your project, you could rewrite them. But if not, it is not an option. Here is how it could be handled:
class UserFactory(factory.django.DjangoModelFactory):
class Params:
generate_account = factory.Trait(
account=factory.RelatedFactory(AccountFactory, factory_related_name='user')
)
Then code above won’t be broken, because default call of UserFactory
won’t instantiate AccountFactory
. To instantiate user with account:
user_with_account = UserFactory(generate_account=True)
You can set account=None
in your Subfactory, see the example here:
https://factoryboy.readthedocs.io/en/stable/recipes.html#example-django-s-profile
user = factory.SubFactory('app.factories.UserFactory', account=None)
I am using Factory Boy to create test factories for my django app. The model I am having an issue with is a very basic Account model which has a OneToOne relation to the django User auth model (using django < 1.5):
# models.py
from django.contrib.auth.models import User
from django.db import models
class Account(models.Model):
user = models.OneToOneField(User)
currency = models.CharField(max_length=3, default='USD')
balance = models.CharField(max_length="5", default='0.00')
Here are my factories:
# factories.py
from django.db.models.signals import post_save
from django.contrib.auth.models import User
import factory
from models import Account
class AccountFactory(factory.django.DjangoModelFactory):
FACTORY_FOR = Account
user = factory.SubFactory('app.factories.UserFactory')
currency = 'USD'
balance = '50.00'
class UserFactory(factory.django.DjangoModelFactory):
FACTORY_FOR = User
username = 'bob'
account = factory.RelatedFactory(AccountFactory)
So I am expecting the factory boy to create a related UserFactory whenever AccountFactory is invoked:
# tests.py
from django.test import TestCase
from factories import AccountFactory
class AccountTest(TestCase):
def setUp(self):
self.factory = AccountFactory()
def test_factory_boy(self):
print self.factory.id
When running the test however, it looks like multiple User models are being create, and I am seeing an integriy error:
IntegrityError: column username is not unique
The documentation does mention watching out for loops when dealing with circular imports, but I am not sure whether that is whats going on, nor how I would remedy it. docs
If anyone familiar with Factory Boy could chime in or provide some insight as to what may be causing this integrity error it would be much appreciated!
I believe this is because you have a circular reference in your factory definitions. Try removing the line account = factory.RelatedFactory(AccountFactory)
from the UserFactory
definition. If you are always going to invoke the account creation through AccountFactory, then you shouldn’t need this line.
Also, you may consider attaching a sequence to the name field, so that if you ever do need more than one account, it’ll generate them automatically.
Change: username = "bob"
to username = factory.Sequence(lambda n : "bob {}".format(n))
and your users will be named “bob 1”, “bob 2”, etc.
To pass result of calling UserFactory
to AccountFactory
you should use factory_related_name
(docs)
Code above works next way:
AccountFactory
for instantiating needsSubFactory(UserFactory)
.UserFactory
instantiates User.UserFactory
after instantiating callsRelatedFactory(AccountFactory)
- Recursion,.. that is broken due to unique username constraint (you probably want to generate usernames via
FuzzyText
orSequence
)
So you need write UserFactory
like this:
class UserFactory(factory.django.DjangoModelFactory):
account = factory.RelatedFactory(AccountFactory, factory_related_name='user')
username = factory.Sequence(lambda a: 'email%[email protected]' % a)
# rest of code
But you can still experience issues with already written tests. Imagine you have in tests places like next:
user = UserFactory()
account = Account(user=user)
Then adding RelatedFactory
will break tests. If you haven’t lots of tests and contributors in your project, you could rewrite them. But if not, it is not an option. Here is how it could be handled:
class UserFactory(factory.django.DjangoModelFactory):
class Params:
generate_account = factory.Trait(
account=factory.RelatedFactory(AccountFactory, factory_related_name='user')
)
Then code above won’t be broken, because default call of UserFactory
won’t instantiate AccountFactory
. To instantiate user with account:
user_with_account = UserFactory(generate_account=True)
You can set account=None
in your Subfactory, see the example here:
https://factoryboy.readthedocs.io/en/stable/recipes.html#example-django-s-profile
user = factory.SubFactory('app.factories.UserFactory', account=None)