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.
Top comments (0)