DEV Community

Cover image for Django testing using pytest
Ousseynou Diop
Ousseynou Diop

Posted on • Edited on

Django testing using pytest

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

Using Pytest

def test_hello_world():
   assert "hello_world" == "hello_world"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Activate our virtual environment

source /venv/bin/activate
Enter fullscreen mode Exit fullscreen mode

Install Django and pytest-django

pip install django pytest-django
Enter fullscreen mode Exit fullscreen mode

Create a new Django project from the terminal

django-admin startproject django_testing .
Enter fullscreen mode Exit fullscreen mode

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

Create a simple django application

python manage.py startapp main_app
Enter fullscreen mode Exit fullscreen mode

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
]
Enter fullscreen mode Exit fullscreen mode

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',
          ],
      },
  },
]
Enter fullscreen mode Exit fullscreen mode

Run the application and make sure everything is working as expected

python manage.py runserver
Enter fullscreen mode Exit fullscreen mode
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.
Enter fullscreen mode Exit fullscreen mode

Make sure DJANGO_SETTINGS_MODULE is defined.

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

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

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
# 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"
Enter fullscreen mode Exit fullscreen mode

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 ==============================
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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 ==============================
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)