Django tests with factory_boy - @rgarrigues - iwoca-logo

Presenter Notes

Current problem

Find a way to create a complex set of data in test environment that is:

- Simple to use

- Easy to read

- Fast to learn

- Be used on any project

Presenter Notes

Several solutions

- "Direct" Django ORM

- Django built-in fixtures (xml/yaml/json)

- Some python packages created for that purpose: factory_boy, model_mummy

Presenter Notes

Overview

Why factory_boy ?

- Simplify object creation for testing purpose

- Avoid painful test code refactoring if your models are changing

- Tests are more readable

- Well adapted to Django, with Django-like syntax, managing all relations

Presenter Notes

How it works

Presenter Notes

How it works: create your factory

 1 # Django "classic" model
 2 class Author(models.Model):
 3     name = models.CharField(max_length=255)
 4     name2 = models.CharField(max_length=255, default='we_dont_care')
 5 
 6 # factory_boy factory definition for that model
 7 class AuthorFactory(factory.django.DjangoModelFactory):
 8     class Meta:
 9         model = Author
10     name = factory.Sequence(lambda n: u'name#%s' % n)

Presenter Notes

Use it

 1 class TestObjectCreation(TestCase):
 2 
 3     def test_without_factory_boy(self):
 4         author = Author.objects.create(name='George RR Martin')
 5         self.assertEqual(author.name, 'George RR Martin')
 6 
 7     def test_with_factory_boy(self):
 8         author = AuthorFactory(name='George RR Martin')
 9         self.assertEqual(author.name, 'George RR Martin')
10 
11     def test_with_factory_boy_auto_creation(self):
12         author = AuthorFactory()
13         self.assertEqual(author.name, 'name#1')
14 
15     def test_with_factory_boy_auto_creation_and_undefined_field(self):
16         author = AuthorFactory()
17         self.assertEqual(author.name2, 'we_dont_care')

Presenter Notes

Simplify nested object creation

Presenter Notes

Simplify nested object creation

 1 class Author(models.Model):
 2     name = models.CharField(max_length=255)
 3 
 4 class Book(models.Model):
 5     author = models.ForeignKey(Author)
 6     title = models.CharField(max_length=255)
 7 
 8 
 9 class TestObjectCreation(TestCase):
10 
11     def test_without_factory_boy(self):
12         author = Author.objects.create(name="George RR Martin")
13         Book.objects.create(author=author, title="Game of Scones")
14 
15     def test_with_factory_boy(self):
16         BookFactory()

Presenter Notes

Simplify nested object creation (2)

 1 class Author(models.Model):
 2     name = models.CharField(max_length=255)
 3 
 4 class Book(models.Model):
 5     author = models.ForeignKey(Author)
 6     title = models.CharField(max_length=255)
 7 
 8 # Factory definition
 9 class AuthorFactory(factory.django.DjangoModelFactory):
10     class Meta:
11         model = Author
12     name = factory.Sequence(lambda n: u'name#%s' % n)
13 
14 class BookFactory(factory.django.DjangoModelFactory):
15     class Meta:
16         model = Book
17     author = factory.SubFactory(AuthorFactory)
18     title = factory.Sequence(lambda n: u'title#%s' % n)

Presenter Notes

Simplify nested object creation (3)

 1 class Author(factory.django.DjangoModelFactory):
 2     name = models.CharField(max_length=666)
 3 
 4 class Book(models.Model):
 5     author = models.ForeignKey(Author)
 6     title = models.CharField(max_length=255)
 7 
 8 
 9 class TestObjectCreation(TestCase):
10 
11     def test_without_factory_boy(self):
12         author = Author.objects.create(name="George RR Martin")
13         Book.objects.create(author=author, title="Game of Scones")
14 
15     def test_with_factory_boy(self):
16         # Specify nested objects values
17         BookFactory(
18             title="Game of Scones",
19             author__name="George RR Martin"
20         )

Presenter Notes

Avoid test code refactoring

Presenter Notes

Avoid test code refactoring

We have just added a new_field in Author model that is required

1 class Author(models.Model):
2     name = models.CharField(max_length=255)
3     new_field = models.CharField(max_length=255)
4 
5 class Book(models.Model):
6     author = models.ForeignKey(Author)
7     title = models.CharField(max_length=255)

Presenter Notes

Avoid test code refactoring (2)

Without factory_boy, we have to update each test that is using Author model

 1 class TestAuthor(TestCase):
 2 
 3     def test_author_1(self):
 4         Author.objects.create(
 5             name = "George RR Martin",
 6             new_field = "new value 1")
 7 
 8     def test_author_2(self):
 9         Author.objects.create(
10             name = "George RR Martin",
11             new_field = "new value 2")
12     ...

Of course, you can create a setUp class to resolve this problem, but how to deal with several TestCase classes that need Author objects ?

A function that factorise Author creation ? Yep. factory_boy already does the job :-)

Presenter Notes

Avoid test code refactoring (3)

With factory_boy, we just have to update the factory.

1 class AuthorFactory(factory.django.DjangoModelFactory):
2     class Meta:
3         model = Author
4     name = factory.Sequence(lambda n: u'author#%s' % n)
5     new_field = factory.Sequence(lambda n: u'new_field#%s' % n)

Each old test is not impacted by the newly added field, and we can use the new_field on new tests.

Presenter Notes

Improve test readability

Presenter Notes

Test readability

- Only focus on what is useful for the test

- Tests are more understandable, as we directly see what is needed

- Don't forget that other people will read your tests for code comprehension

Presenter Notes

Test readability (2)

Example with models that contain more fields

 1 class Author(models.Model):
 2     name = models.CharField(max_length=255)
 3     birth_date = models.DateTimeField()
 4     favorite_color = models.CharField(max_length=50)
 5     favorite_expression = models.CharField(max_length=1050)
 6     favorite_breakfast_cereals = models.CharField(max_length=111)
 7 
 8 class Book(models.Model):
 9     author = models.ForeignKey(Author)
10     title = models.CharField(max_length=255)
11     publication_date = models.DateTimeField()
12     category = models.CharField(max_length=255)

Presenter Notes

Test readability (3)

Too many useless data impact test readability, and are... useless !

 1 class TestBookAuthorFavoriteCereals(TestCase):
 2 
 3     def test_without_factory_boy(self):
 4         author = Author.objects.create(
 5             name="George RR Martin",
 6             birth_date=datetime(1948, 9, 20),
 7             favorite_color="black",
 8             favorite_expression="Breakfast is coming !",
 9             favorite_breakfast_cereals="Honey smacks"
10         )
11         book = Book.objects.create(
12             author = author,
13             title="Game of scones",
14             publication_date=datetime(2014, 9, 9),
15             category="cooking"
16         )
17 
18         query = Book.objects.filter(
19             category="cooking",
20             author__favorite_breakfast_cereals="Honey smacks")
21         self.assertEqual(query.count(), 1)

Presenter Notes

Test readability (4)

Less infos, and you focus on what is really important for your test.

 1 class TestBookAuthorFavoriteCereals(TestCase):
 2 
 3     def test_with_factory_boy(self):
 4         BookFactory(
 5             category="cooking",
 6             author__favorite_breakfast_cereals="Honey smacks"
 7         )
 8 
 9         query = Book.objects.filter(
10             category="cooking",
11             author__favorite_breakfast_cereals="Honey smacks")
12         self.assertEqual(query.count(), 1)

Presenter Notes

Manage all Django ORM relations

Presenter Notes

Django relations: ForeignKey

1 class Book(models.Model):
2     author = models.ForeignKey(Author)
3 
4 class BookFactory(factory.django.DjangoModelFactory):
5     class Meta:
6         model = Book
7     author = factory.SubFactory(AuthorFactory)

Presenter Notes

Django relations: OneToOneField

Similar to ForeignKey

1 class Book(models.Model):
2     author = models.OneToOneField(Author)
3 
4 class BookFactory(factory.django.DjangoModelFactory):
5     class Meta:
6         model = Book
7     author = factory.SubFactory(AuthorFactory)

Presenter Notes

Django relations: ManyToManyField

 1 class Author(models.Model):
 2     books = models.ManyToManyField(Book)
 3 
 4 class AuthorFactory(factory.django.DjangoModelFactory):
 5     class Meta:
 6         model = Book
 7 
 8     @factory.post_generation
 9     def add_books_to_author(self, create, extracted, **kwargs):
10         if not create:
11             return
12         if extracted:
13             for book in extracted:
14                 self.books.add(book)
15 
16 # In a test
17 AuthorFactory(add_books_to_author=[book1, book2, book3])

Also possible to manage ManyToManyFields with intermediary tables, GenericForeignKeys, ...

Presenter Notes

Extras

Presenter Notes

Extra: fancy data generation

factory.fuzzy module to generate random data for some defined types:

1 class FuzzyFactory(factory.django.DjangoModelFactory):
2     class Meta:
3         model = FuzzyModel
4 
5     random_int = factory.fuzzy.FuzzyInteger(30, 99)
6     random_char = factory.fuzzy.FuzzyChoice(['Low', 'Medium', 'High'])
7     [...]

Presenter Notes

Extra: "real" data generation

Faker python module can be used to generate "real" data

1 class AuthorFactory(factory.django.DjangoModelFactory):
2     class Meta:
3         model = Author
4 
5     name = factory.LazyAttribute(lambda x: faker.name())
6     phone_number = factory.LazyAttribute(lambda x: faker.phone_number())

Faker can generate addresses, datetimes, ...

Presenter Notes

Extra: LazyAttribute

We can generate some fields depending on other fields

 1 class AuthorFactory(factory.Factory):
 2     class Meta:
 3         model = User
 4     name = factory.Sequence(lambda n: 'name%s' % n)
 5     email = factory.LazyAttribute(lambda o: '%s@example.com' % o.name)
 6 
 7 class TestLazyAttributes(TestCase):
 8 
 9     def test_lazy(self):
10         author = AuthorFactory()
11         self.assertEqual(author.name, 'name1')
12         self.assertEqual(author.email, 'name1@example.com')

Presenter Notes

Extra: build vs create

Build: create python object

Create: build + database saving

 1 class TestAuthorCreation(TestCase):
 2 
 3     def test_build(self):
 4         AuthorFactory.build()
 5         self.assertEqual(Author.objects.count(), 0)
 6 
 7     def test_creation(self):
 8         # Equivalent to AuthorFactory()
 9         AuthorFactory.create()
10         self.assertEqual(Author.objects.count(), 1)

Presenter Notes

Extra: batch build/create

 1 class TestBatchAuthorOperations(TestCase):
 2 
 3     def test_build(self):
 4         authors = AuthorFactory.build_batch(size=20)
 5         self.assertEqual(len(authors), 20)
 6         self.assertFalse(Author.objects.exists())
 7 
 8     def test_create(self):
 9         AuthorFactory.create_batch(size=20)
10         self.assertEqual(Author.objects.count(), 20)

Presenter Notes

Tips

Presenter Notes

Tips: data migration conflicts

 1 class Author(models.Model):
 2     name = models.CharField(max_length=255, unique=True)
 3 
 4 class AuthorFactory(factory.django.DjangoModelFactory):
 5     class Meta:
 6         model = Author
 7     name = factory.Sequence(lambda n: u'author#%s' % n)
 8 
 9 # Imagine you have created an author in an initial data migration
10 # with name='author#1'
11 
12 class TestAuthorCreation(TestCase):
13 
14     def test_author(self):
15         # IntegrityError because the factory will try to create
16         # an author with name='author#1'
17         AuthorFactory()

Presenter Notes

Tips: data migration conflicts (2)

Use django_get_or_create

 1 class AuthorFactory(factory.django.DjangoModelFactory):
 2     class Meta:
 3         model = Author
 4         django_get_or_create = ('name', )
 5     name = factory.Sequence(lambda n: u'author#%s' % n)
 6 
 7 class TestAuthorCreation(TestCase):
 8 
 9     def test_author(self):
10         query = Author.objects.filter(name='author#1')
11         self.assertEqual(query.count(), 1)
12 
13         # No more IntegrityError
14         AuthorFactory()
15         self.assertEqual(query.count(), 1)

Presenter Notes

Tips: Anti-patterns

DON'T create a factory per "context", like BookFactory, BookWithAuthorFactory, BookWithAuthorAndAddressFactory, ...

-> create util functions that uses different factories, depending
on the context.

DON'T use it outside of a test environment:

-> be really careful on random generated fields (!).

DON'T define ALL model fields in your factories, just mandatory ones and without default values:

-> Always better to set data (if default is not wanted) than unset.

Presenter Notes

Tips: How to switch to factory_boy ?

Not switch directly all your tests

Begin to create new factories on your new tests, in a separate file for example

1 .
2 +-- manage.py
3 +-- settings
4 +-- your_app
5 |   +-- admin.py
6 |   +-- factories.py  <---
7 |   +-- models.py
8 |   +-- urls.py
9 |   +-- views.py

When you modify/fix/add tests in an app, update them with factories instead of direct orm calls

Presenter Notes

iwoca-logo is looking for factory boys !!

Presenter Notes