Mocking database calls in Django view tests
It took me a long time to understand the first thing about mocking in unit tests. The next few posts are intended to be a future reference to myself. Maybe you find them useful, or better, you can tell me how to do this better.
I created a simple Django project to document my solutions in working code: https://github.com/FlowFX/sturdy-potato. For the purpose of these posts I will use the models, views and tests from this project. All views are class-based views.
Why mocking? Because I want fast tests. Database calls are especially slow, and for many tests, it is not necessary to actually write to or load from the database. So I want to avoid these.
A simple view test
The Potato
model has two attributes: weight
and variety.
from django.db import models
from django.core.validators import MinValueValidator
class Potato(models.Model):
"""The Potato model."""
slug = models.SlugField(unique=True)
weight = models.IntegerField(validators=[MinValueValidator(1)])
variety = models.CharField(max_length=255)
The URL for the detail page:
from django.conf.urls import url
from potatoes import views
urlpatterns = [
[...]
url(r'^potatoes/(?P<pk>[0-9]+)/$', views.PotatoDetailView.as_view(), name='detail'),
]
The view subclasses the DetailView:
from potatoes.models import Potato
from django.views.generic import DetailView
class PotatoDetailView(DetailView):
"""Detail view for the Potato object."""
model = Potato
An simple way of testing this view is using the Django test client.
When using pytest, the test client is made available as a fixture by the pytest-django plugin. Because I don’t use Django/unittest’s TestCase
, I need to make the test database available with the @pytest.mark.django_db
decorator.
from django.urls import reverse
from potatoes.factories import PotatoFactory
import pytest
@pytest.mark.django_db
def test_detail_view(client):
"""Test the detail view for a Potato object with the Django test client."""
# (1) GIVEN a Potato object in the database
potato = PotatoFactory.create() # saves to database
# (2) WHEN calling the DetailView for this object
url = reverse('detail', kwargs={'pk': potato.id})
response = client.get(url)
content = response.content.decode()
# (3) THEN it shows the potato's ID and it's type
assert response.status_code == 200
assert str(potato.weight) in content
assert potato.variety in content
What’s happening here?
- Using Factory Boy’s DjangoModelFactory, a test
Potato
is created and written to the database. - The test client does a
GET
request to the URL of the details page of this Potato. This reads from the database. - It is checked whether the Potato’s attributes are displayed on the page.
This test hits the database twice, although I only want to test whether my view (and kind of my template) works or not. I’m pretty sure the Django ORM works fine.
View test with mock
In the test above, the object is only saved to the database so that the DetailView can read it from there. The method that reads from the database is the PotatoDetailView
’s get_object
method.
In order to avoid the database request, I can use a so-called monkey patch that provides a return value for the method, without hitting the database.
from mock import patch
def test_detail_view(client):
"""Test the detail view for a Potato object with the Django test client."""
# (1) GIVEN a Potato object
potato = PotatoFactory.build() # not saved to the database
# (2) monkey-patching
with patch.object(PotatoDetailView, 'get_object', return_value=potato):
# (3) WHEN calling the DetailView for this object
url = reverse('detail', kwargs={'pk': 1234}) # pk can be anything
response = client.get(url)
content = response.content.decode()
# THEN it shows the potato's ID and it's type
assert response.status_code == 200
assert str(potato.weight) in content
assert potato.variety in content
This is the same test with just a few changes.
- The Potato instance is not saved to the database. (Check Factory Boy’s
build()
vs.create()
methods.) - This is the fun part. The patch
patch.object(PotatoDetailView, ‘get_object’, return_value=potato)
takes thePotatoDetailView
and, first of all, disables theget_object
method. Second, it replaces the method by something that always returns thepotato
instance. Always. - No matter what primary key we call the detail view with, it will always receive the test
potato
to work with. Which is really all we need to assert stuff.
There is no database call, no need for the django_db mark, just more speed.
For a ListView, the method that has to be replaced by the patch is get_queryset
. Check out test_list_view
in the example project.
Categories: #Tech Tags: #Django #Mock #Pytest #Unit Test #Testing