"Magic" Model Pytest fixtures

When building and testing Django applications we find ourselves importing models fairly frequently.

from third_party.models import UsefulModel

from my_project.my_app.models import SomeModel
from my_project.my_other_app.models import SomeOtherModel
# etc

Most Django developers are used to this — the explicit imports don't feel like a big deal in production code.

Let's swap

But what if you load models dynamically and you don't know which app they will be coming from?

The most common use-case is swappable user models. Normally to access the user model we would write:

from django.contrib.auth import get_user_model

User = get_user_model()

That would be a bit annoying to type everywhere in your test modules on top of all the production ones...
Luckily, pytest-django already offers a django_user_model fixture that converts this into a neat little fixture, so in your tests you can simply do:

# from the pytest-django docs:
def test_new_user(django_user_model):
    django_user_model.objects.create(
        username="someone", password="something")

But I like mine better!

Overriding User is one example, but maybe you have more interchangeable apps, and in your app context there is no guarantee where a model could really be imported from... Maybe the project will be using my_amazing.blog_app.models.Comment or it may be using my_legacy.blog_app.models.Comment.

Django has you covered with that, as you can request a model just by label and name:

from django.apps import apps

Comment = apps.get_model("blog_app.Comment")

This will ensure the correct Comment model is imported and you don't really need to know where it comes from.

Oooh, but it's just like get_user_model all over again 😤😤😤

You could make things a bit easier by removing the need to import get_model...

@pytest.fixture
def get_model():
    from django.apps import apps
    return get_model

def test_comment(get_model):
    comment = get_model("blog_app.Comment")()
    # ...

Or you can even create a fixture that will do this for you:

from django.apps import apps

@pytest.fixture
def Comment():
    return get_model("blog_app.Comment")

def test_comment(Comment):
    comment = Comment()

That looks much neater already!

What if you have many models that could be overridden like this?
Putting this concept on steroids, we could add something like the following to our conftest.py:

from django.apps import apps

def create_model_fixture(model):
    def _fixture():
        return model

    _fixture.__name__ = model._meta.object_name
    return _fixture


for app_label, models in apps.all_models.items():
    for model in models.values():
        vars()[model._meta.object_name] = pytest.fixture(
            create_model_fixture(model)
        )

Then in our tests we can access any model by it's object name, like:

def test_stuff(Comment, BlogPost):
    comment = Comment()
    post = BlogPost()
    # ... assert some stuff

Gotcha/DISCLAIMER: The code above is a bit "magic" and assumes that there are no models with the same name registered at the same time. If that is a potential issue you could amend the example above to name your fixtures more explicitly, ex to my_blog_comment_model vs my_review_comment_model etc.

That's it. ✨ ✨ Happy testing! ✨✨