Is it considered good practice using too many factories in pytest?

1.3k Views Asked by At

I am trying to write tests for Django/DjangoREST project. I have decided to use pytest. I have poor experience in writing tests for Django projects specifically. So I am confused now.

Here is an example:

@pytest.mark.django_db
def test_some_view(
    api_client,
    simple_user,
    model1_factory,
    model2_factory,
    ...  # many other model factories
    modelN_factory
):
    # ...
    # creating here other objects that really depends on each other
    # ...
    model2_obj = ...   # model2 object on its own side depends on model3, model4... and so on

    model1_objs = []
    for i in range(10):
        model1_objs.append(model1_factory(some_field=100, some_model2_rel=model2_obj)
    assert len(model1_objs) == 1, "Created items with duplicate `some_field`"

As you can see I have too many factories to be used in one test. But looking at my model structure right now, I can't think of a better way. Is it ok to use so many factories for one test? Or should I find some issues related to my tables' relations?

Any help is appreciated. Thanks in advance

1

There are 1 best solutions below

3
On BEST ANSWER

The main goal of factory_boy is getting rid of fixtures; its typical use case is:

  1. Design your Factory classes, which are basically recipes for getting a "realistic" object instance
  2. In your test, call only the factories you need, specifying just the parameters for that test case.

As I understand it, pytest fixtures are intended for "setting up the test environment": booting the database, mocking an external service, etc.; creating objects inside the database isn't a good fit for them.

The way I'd write your code would be the following:

# factories.py
import factory
import factory.fuzzy

from . import models

class DivisionFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = models.Division
    name = factory.Faker('company')

class EmployeeFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = models.Employee
    username = factory.Faker('username')
    name = factory.Faker('name')
    employee_id = factory.Sequence(lambda n: 'EM199%06d' % n)
    division = factory.SubFactory(DivisionFactory)
    role = factory.fuzzy.FuzzyChoice(models.Employee.ROLES)
    hired_on = factory.fuzzy.FuzzyDate(
        start_date=datetime.date.today() - datetime.timedelta(days=100),
        end_date=datetime.date.today() - datetime.timedelta(days=10),
    )

We have a factory for an employee, and one for a division - and each employee gets assigned to a division. Every mandatory field is provided; if we need to make specific factories for some object profiles, this can be added through either subclassing or using traits.

We can now write our tests, passing only the details required for the test:

# tests.py

@pytest.mark.django_db
def test_get_new_hire(api_client):
    employee = factories.EmployeeFactory(
        hired_on=datetime.date.today(),
        division__name="Finance",
    )
    data = api_client.get(f'/employees/{employee.username}')
    assert data['division'] == "Finance"
    assert data['orientation_status'] == 'pending'

As a side note, wouldn't it make more sense to use Django's test runner directly? It's more finely tuned for Django internals: each test can be natively wrapped in a sub-transaction for performance, the test client provides helpers for in-depth introspection of view results, etc.