Search code examples
unit-testingpython-3.xmockingdjango-querysetdjango-unittest

Unit test checking if database queries are correct – what can be mocked?


Let's say I have an Article model, like this:

from django.db import models

class Article(models.Model):
    author = models.CharField(max_length=100)
    title = models.CharField(max_length=200)
    body = models.TextField()

This is naively simple compared to my actual usage (author should be a ForeignKey to another model, etc.), but this way it's more clear.

Imagine I want to list titles of all the articles by certain authors, yet keeping each author's pieces together. It could be represented as a list of lists:

def get_beatles_articles_titles():
    beatles = [
        "John Lennon",
        "Paul McCartney",
        "George Harrison", 
        "Ringo Starrr",
    ]
    return [article.author for author in beatles 
            for article in Article.objects.filter(author=author)]

Oh, a nested list comprehension, so our method isn't that simple. There's a big chance of a bug being somewhere here, so we should test it somehow! The easiest solution seems to be creating some Article instances corresponding to each author (and saving them in the database) and check if all of them are properly fetched.

from django.test import TestCase

from models import Article
from views import get_beatles_articles_titles

class ArticlesTitlesTestCase(TestCase):
    def test_that_every_beatles__article_is_fetched(self):
        Article.objects.create(author="John Lennon", title="John's")
        Article.objects.create(author="Paul McCartney", title="Paul's")
        Article.objects.create(author="George Harrison", title="George's")
        Article.objects.create(author="Ringo Starr", title="Ringo's")

        self.assertEqual(get_beatles_articles_titles(), [
            ["John's"],
            ["Paul's"],
            ["George's"],
            ["Ringo's"]
        ])

Running that test we can see that there's a typo in our original code, so it proved its usefulness.

However, accessing database is frowned upon in favor of mocking things (no wonder, I've experienced that time difference can be significant). What can be mocked in the test above? I'm particularly anxious about correctness of my .filter query (it might get pretty complex), so I don't want to guess QuerySets a DB would give me.

Ideally, I'd like to use something like this (pseudo code follows):

johns_article = Article(author="John Lennon")
fake_query = MockQuery(author__contains="John")
assertTrue(fakeQuery.contains(johns_article))

Solution

  • accessing database is frowned upon in favor of mocking things

    Why is this?? Common concerns are:

    • test speed
    • test reliability
    • test flakiness
    • test parallelization
    • fixture creation
    • data management in general

    If you need to test your database interaction the only way would be to run your test against a database. Django has you covered and has created a framework that addresses all the concerns listed above. The django TestCase handles all of this for you. It manages a test database, provides tools for provisioning, executes all tests in a transactions and quickly cleans up by rolling back after every transaction.

    I definitely agree that for quick tests they should be done on the unit level and stub all collaboration and file/socket access, but in this case django should have you covered.

    You're already using the django database, which is being provisioned for every test run, so why not access it?

    So basically your test is right on, django created the tools for it, it is not an anti pattern at all. A common strategy is to have multiple tiers of tests. You could have a "unit test" or "small test" framework and test runner that does no IO where most of your tests live, where all collaboration is mocked/stubbed. Then implement a number of tests in django TestCase to test database interaction.


    If youre starting with testing/django there are a couple of things that could help in the long run:

    1. Profile your queries, get_beatles_articles_titles can be optimized and done with less queries
    2. use the bulk_create method instead of multiple create methods
    3. Using an abstraction to instantiate your test models, my fav is factory-boy. This will help insulate you from changes to what is required to create models, plus it allows you to create sane, and dynamic defaults, so the creation process is a lot more succinct.