Factory Boy: fixture files be gone!

Jul 25, 2015 16:50 · 746 words · 4 minutes read django factory-boy fixtures

Personally I like to use factory boy for fixtures and it is worth checking out. Rather than regurgitating the docs, I am going to quickly cover my approach to unit testing in Django, a bit on factory boy and offer a glimpse on how I use factory boy.

Why not Fixture Files or Data Migrations?

Well, firstly, the standard complaints about Django fixture files applies. JSON is unmaintainable with migrations, you can’t comment JSON and keeping it DRY is impossible - it is a naive data dump. This is a problem with how fixtures are implemented in Django, not necessarily about the concept in general.

I have always liked to be able to look at a test case and know exactly what it is interacting with. When the data is in a fixture file, it is too far removed from the test to at a cursory glance understand what data is being interacted with. It also increases the barrier to contribution if you have to learn the fixtures before you can write tests.

I also like to leave data migrations alone outside of production migration considerations.

ORM Only

I started off using the ORM, which gave me ultimate control over what is created and required in a unit test. But this does make test files verbose and inverts the order of fixture construction when it comes to reasoning about the test. For example to test a booking in a restaurant we need to create a Restaurant, a Table, a Seating and then we can finally create our test Booking when all we care about is the booking, this is unnecessarily verbose and is rather distracting.

With it being so verbose, we might decide to move everything but the Booking creation into the TestCase setUp() method, but we inevitably end up needing to alter those objects. Or we might only need something for a subset of tests, but wanting to keep things less verbose, we move it into the setUp() method as well. Or we want to create a new TestCase, but we already have all the setUp() elsewhere so we create a base TestCase with our setup and inherit from it or copy paste it.

All things that I have contended with in the past and now avoid.

Factory Boy

We can instead use factory boy to model the relationships between models along with default fixture attributes using Python. We can then create a Booking via our BookingFactory and it will go off and create all the required related objects via sub factories and populate the attributes for us. We can provide different data to the boilerplate. We can programatically generate attributes, for example unique emails every time we as the UserFactory to create us a User. Excellent.

Factory Boy Observations

I often end up creating mini factory functions to further abstract away a fixture. For example, I could create a function small_booked_restaurant() which uses the factories to build up a set of objects that represent a small booked restaurant.

I never rely on fixture data in a test, only for the inconsequential data. So if I want to test that when I create a booking at 18:00 at the Ritz London for 4 on the 1st of August 2015, the string representation is "Ritz London; table for 4 at 18:00 on the 01 Aug '15". My BookingFactory with the default fixture data may mean that this test will pass, this is brittle. The fixtures may get changed and it isn’t entirely obvious when looking at a test case why it passes. Instead I will specify those values as explicit attributes when using the BookingFactory to get a Booking. This way the fixture data can change and my test does not break and I have explicitly stated in the test that a booking created with those values does result in rather than relied on the fixture data.

This approach to fixtures via factories has allowed me to not rely on JSON files; remove the vast majority of ORM boilerplate in tests; eliminate the usage of setUp() for creating fixture data and use fixtures for what they do best, provide inconsequential boilerplate data.

It’s not all roses

Factory Boy tracebacks are not the nicest to deal with, this is something I am getting used to and once I had learnt the Factory Boy API, something I am less stumped by.

Still check it out if you have never used it, it is much better than the alternatives.