axil's blog
  • Home
  • Categories
  • Tags
  • Archives

Pytest vs standard django tests

By default both testing frameworks rely on transactions for test cases isolation. But the details are different.

To see how django test framework works consider the following testcase:

from django.test import TestCase
from django.contrib.auth.models import User

def get_users():
    return repr(list(u.username for u in User.objects.all()))

class UserTest(TestCase):
    def setUp(self):
        User.objects.create(username='a')
        print('setup: ' + get_users())

    def test1(self):
        User.objects.create(username='b')
        print('test1: ' + get_users())

    def test2(self):
        User.objects.create(username='c')
        print('test2: ' + get_users())

It prints ("pytest test.py"):

setup: ['a']
test1: ['a', 'b']
setup: ['a']
test2: ['a', 'c']

Which corresponds to the following transaction layout:

start transaction -> setup -> test1 -> rollback ->
start transaction -> setup -> test2 -> rollback

By default in pytest the same algorithm is used:

import pytest
from django.contrib.auth.models import User

def get_users():
    return repr(list(u.username for u in User.objects.all()))

@pytest.fixture
def first_user(db):
    User.objects.create(username='a')
    print('setup: ' + get_users())

@pytest.mark.django_db
def test1(first_user):
    User.objects.create(username='b')
    print('test1: ' + get_users())

@pytest.mark.django_db
def test2(first_user):
    User.objects.create(username='c')
    print('test2: ' + get_users())

setup: ['a']
test1: ['a', 'b']
setup: ['a']
test2: ['a', 'c']

Now consider a situation where the setup function takes a long time to run. In pytest it is possible to do such a trick then:

import pytest
from django.contrib.auth.models import User

def get_users():
    return repr(list(u.username for u in User.objects.all()))

@pytest.fixture(scope='module')
def django_db_setup(django_db_setup, django_db_blocker):
    with django_db_blocker.unblock():
        User.objects.create(username='a')
        print('setup: ' + get_users())

@pytest.mark.django_db
def test1(first_user):
    User.objects.create(username='b')
    print('test1: ' + get_users())

@pytest.mark.django_db
def test2(first_user):
    User.objects.create(username='c')
    print('test2: ' + get_users())

which prints:

setup: ['a']
test1: ['a', 'b']
test2: ['a', 'c']

So the transactions layout is like that:

setup ->
start transaction -> test1 -> rollback ->
start transaction -> test2 -> rollback

It can be used with "--reuse-db" flag to save db creation time, but at a cost of losing isolation between executions of the setup function in the subsequent test runs. To avoid 'User a already exists' situation we can change "User.objects.create" to "User.objects.get_or_create", but it leaves the db in a dirty state.

To keep test database clean, manual deletion of all records created in setup function is necessary, but doing so is error-prone (you need to keep in mind that you should delete each record you create in setup). In pytest cleanup is supposed to be implemented in the same setup function using the generators technique:

import pytest
from django.contrib.auth.models import User

def get_users():
    return repr(list(u.username for u in User.objects.all()))

@pytest.fixture(scope='module')
def django_db_setup(django_db_setup, django_db_blocker):
    with django_db_blocker.unblock():
        User.objects.create(username='a')
        print('setup: ' + get_users())
    yield
    with django_db_blocker.unblock():
        User.objects.filter(username='a').delete()
        print('cleanup: ' + get_users())

@pytest.mark.django_db
def test1():
    User.objects.create(username='b')
    print('test1: ' + get_users())

@pytest.mark.django_db
def test2():
    User.objects.create(username='c')
    print('test2: ' + get_users())


setup: ['a']
test1: ['a', 'b']
test2: ['a', 'c']
cleanup: []

The technique shows how in pytest one can exclude the setup function from the transaction rollback mechanism so that the setup is only run once for the test suite which means lower testing time.

There are other ways to get such an effect, but this one is most close to the "letter of the documentation".

  • « Django templates to Jinja2 dictionary
  • A Comprehensive Guide to NumPy Data Types »
Comments
comments powered by Disqus

Published

Jun 8, 2017

Last Updated

2020-07-10 17:30:42.287331+07:00

Category

software

Tags

  • django 2
  • pytest 1
  • python 4
  • Powered by Pelican. Theme: Elegant by Talha Mansoor