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! ✨✨