loading...
Cover image for Django testing using pytest

Django testing using pytest

xarala221 profile image Ousseynou Diop Updated on ・5 min read

Originally posted on my blog

Introduction

In our previous article we've discussed writing simple tests in Django.
In this article, we'll go one step further.

Pytest helps you write better programs.

The Pytest framework makes it easy to write small tests, yet scales to support complex functional testing for applications and libraries.

Here is an example of a simple test

def inc(x):
   return x + 1


def test_answer():
   assert inc(3) == 5

Execute it, what is the result?

A great developer should test his code before the client or user interaction.
Testing is not an easy task as you may think.
Writing advanced tests is very challenging but pytest makes it a lot easier than before.

Why Pytest

You can test your Django application without using a Library but pytest offers some features that are not present in Django’s standard test mechanism: :

  • Detailed info on failing assert statements (no need to remember self.assert* names);
  • Auto-discovery of test modules and functions;
  • Modular fixtures for managing small or parametrized long-lived test resources;
  • Can run unit test (including trial) and nose test suites out of the box;
  • More about pytest here

Pytest vs Django unit testing Framework

Here is a basci comparaison

from django.test import TestCase


class TestHelloWorld(TestCase):
   def test_hello_world(self):
       self.assertEqual("hello world", "hello world")

Using Pytest

def test_hello_world():
   assert "hello_world" == "hello_world"

Setting Up Pytest Django

pytest-django is a plugin for pytest that provides a set of useful tools for testing Django applications and projects.

You can find the final code here

Create a new virtual environment

mkdir django_testing_using_pytest && cd django_testing_using_pytest
virtualenv venv # this command will create a virtual environment called venv

Activate our virtual environment

source /venv/bin/activate

Install Django and pytest-django

pip install django pytest-django

Create a new Django project from the terminal

django-admin startproject django_testing .

Don't forget to put the dot(.) at the end.

Create a simple django application

python manage.py startapp main_app

Register our newly created app

# settings.py
INSTALLED_APPS = [
  'django.contrib.admin',
  'django.contrib.auth',
  'django.contrib.contenttypes',
  'django.contrib.sessions',
  'django.contrib.messages',
  'django.contrib.staticfiles',
  'main_app.apps.MainAppConfig' # add this
]

Change the templates directory

# settings.py
TEMPLATES = [
  {
      'BACKEND': 'django.template.backends.django.DjangoTemplates',
      'DIRS': [os.path.join(BASE_DIR, 'templates')],  # add this
      'APP_DIRS': True,
      'OPTIONS': {
          'context_processors': [
              'django.template.context_processors.debug',
              'django.template.context_processors.request',
              'django.contrib.auth.context_processors.auth',
              'django.contrib.messages.context_processors.messages',
          ],
      },
  },
]

Run the application and make sure everything is working as expected

python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).

You have 17 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.

May 01, 2020 - 20:28:17
Django version 3.0.5, using settings 'django_testing.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

Make sure DJANGO_SETTINGS_MODULE is defined.

Create a file called pytest.ini in your project root directory that contains:

touch pytest.ini
# pytest.ini
[pytest]
DJANGO_SETTINGS_MODULE = django_testing.settings
# -- recommended but optional:
python_files = tests.py test_*.py *_tests.py

Let's run our test suite

$ pytest
============================= test session starts ==============================
platform linux -- Python 3.7.5, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/username/projects/username/source-code/django_testing_using_pytest, inifile: pytest.ini
plugins: django-3.9.0
collected 0 items

You may ask why run test suite instead of Django manage.py command, here is the answer :

  • Less boilerplate: no need to import unittest, create a subclass with methods. Just write tests as regular functions.
  • Manage test dependencies with fixtures.
  • Run tests in multiple processes for increased speed.
  • There are a lot of other nice plugins available for pytest.
  • Easy switching: Existing unittest-style tests will still work without any modifications.

See the pytest documentation for more information on pytest.

Test Discovery

The first thing that pytest provides is test discovery. Like nose, starting from the directory where it is run, it will find any Python module prefixed with test* and will attempt to run any defined unittest or function prefixed with test*. pytest explores properly defined Python packages, searching recursively through directories that include init.py modules.

Here is an example :

├── django_testing
│   ├── asgi.py
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── main_app
│   ├── admin.py
│   ├── apps.py
│   ├── __init__.py
│   ├── migrations
│   │   ├── __init__.py
│   │   └── __pycache__
│   │       └── __init__.cpython-37.pyc
│   ├── models.py
│   ├── test_contact.py # checked for tests
│   ├── test_blog.py # checked for tests
│   ├── test_todo.py # checked for tests
│   └── views.py

Let's write a test for our model

# models.py
from django.db import models

class Contact(models.Model):
   first_name = models.CharField(max_length=150)
   last_name = models.CharField(max_length=150)
   phone = models.CharField(max_length=150)
   email = models.CharField(max_length=150, blank=True)
   created_at = models.DateTimeField(auto_now_add=True)

   def __str__(self):
       return self.phone
# tests.py

import pytest
from .models import Contact


@pytest.mark.django_db
def test_contact_create():
   contact = Contact.objects.create(
       first_name="John",
       last_name="Doe",
       email="john@gmail.com",
       phone="00221 70 992 33 43"
   )
   assert contact.email == "john@gmail.com"
   assert contact.phone == "00221 70 992 33 43"

Run the test

pytest
============================= test session starts =============================
platform linux -- Python 3.7.5, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
django: settings: django_testing.settings (from ini)
rootdir: /home/username/projects/username/source-code/django_testing_using_pytest, inifile: pytest.ini
plugins: django-3.9.0
collected 1 item

main_app/tests.py .                                                     [100%]

============================== 1 passed in 0.27s ==============================

Our test passed.

Let's break our code and run the test again

# tests.py

import pytest
from .models import Contact


@pytest.mark.django_db
def test_contact_create():
   contact = Contact.objects.create(
       first_name="John",
       last_name="Doe",
       email="john@gmail.com",
       phone="00221 70 992 33 43"
   )
   assert contact.email == "john@hotmail.com"
   assert contact.phone == "00221 70 992 33 43"

Run the test

================================== FAILURES ===================================
_____________________________ test_contact_create _____________________________

   @pytest.mark.django_db
   def test_contact_create():
       contact = Contact.objects.create(
           first_name="John",
           last_name="Doe",
           email="john@gmail.com",
           phone="00221 70 992 33 43"
       )
>       assert contact.email == "john@hotmail.com"
E       AssertionError: assert 'john@gmail.com' == 'john@hotmail.com'
E         - john@hotmail.com
E         ?      ^^^
E         + john@gmail.com
E         ?      ^

main_app/tests.py:14: AssertionError
=========================== short test summary info ===========================
FAILED main_app/tests.py::test_contact_create - AssertionError: assert 'john...
============================== 1 failed in 0.33s ==============================

Conclusion

In this article we've how to setup Pytest with Django, you can do more stuff with Pytest it's well documented.
We'll write test coverage in our next article.

Thanks for reading, See you next.

Posted on by:

xarala221 profile

Ousseynou Diop

@xarala221

Freelance ❯ Fullstack Dev ❯ Python Lover ❯ Django ❯Js ❯ Node ❯ React ❯ GraphQL🔥

Discussion

markdown guide