How to Unit test Django views & models with Pytest
By: Muhammed Ali
Tags: Testing, Python, Pytest, Django, Tdd

Introduction

Because there will be a constant need to change and optimize code there are times where the code that is added or removed can cause unknown problems and bugs in the codebase which can be very detrimental especially if it is a live project. Hence testing ensures the codebase is covered all around and if there are new bugs developed by your code change they can easily be detected by the test suite, that way you can make changes to the code without worrying if something is broken.

Another reason you’d want to write tests is it improves your speed in your development process, you wouldn’t have to start testing things manually. There are different types of tests; Integration Testing, Unit Testing, Regression Testing, etc. you can read about them in this article, in this article we are going to be working with unit tests.

In this tutorial, I am going to assume you have python installed on your system, if you don’t you can just do a quick google search.

Prerequisites

Basic knowledge of:

Django and Python

What is Pytest?

Pytest is a framework that makes building simple and scalable tests easy. I won’t go into details about the inner workings of Pytest. Pytest helps you write tests that are very scalable, with ease. We’ll just go straight to solving problems.

The code snippet is in the repository so you can check that out in case you have problems with the file arrangement.

How to set up Pytest with Django

First of all, create a new virtual environment for the project.

$ virtualenv env

activate the virtual environment with

On macOS and Linux:

$ source env/bin/activate

On Windows:

$ .\env\Scripts\activate

Then, install Django, pytest-cov and pytest-django.

Pytest-django is a plugin for Pytest that provides a set of useful tools for testing Django applications and projects i.e it helps you use pytest on Django.

Pytest-cov is used for checking the test coverage(test coverage is the amount of code that is covered by the test suite) of the project. mixer is used to mock the database.

(env)$ pip install django pytest-django pytest-cov mixer

then run:

(env)$ django-admin startproject django_pytest

cd into the directory that contains manage.py and run 👇

(env) $ python manage.py startapp blog

add ‘blog’ to settings.py

... INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'blog', #new ]

...

We are going to be testing a blogging application… in …blog/models.py directory

from django.db import models

from django.utils import timezone class Post(models.Model):

author = models.CharField(max_length=200)

title = models.CharField(max_length=200)

content = models.CharField(max_length=200

date_posted = models.DateTimeField(default=timezone.now) def __str__(self):

return self.title

Update the project urls.py to point to the post content.

from django.contrib import admin

from django.urls import path

from blog import views #new urlpatterns = [

path('admin/', admin.site.urls),

path('<int:pk>', views.post_content, name="content") #new

]

blog/views.py, write views to display the blog content

from django.shortcuts import get_object_or_404, render

from .models import Post def post_content(request, pk):

post = get_object_or_404(Post, pk=pk)

return render(request, "post_content.html", {'post':post})

create templates folder in the blog directory and create post_content.html folder in it.

add the below code to templates/post_content.html

{{ post.content }}

then make migrations and migrate.

To test that the site is working let's open the shell and add some posts.

run

python manage.py shell

run the follow below now after the other

>>> from blog.models import Post

>>> from datetime import datetime

>>> Post.objects.create(author="Muhammed Ali",title="Testing Django app", content="just a simple blog",date_posted=datetime.now()).save()

to check if it actually saved

>>> Post.objects.get(pk=1)

It should show <Post: Testing Django app> or anything you put as the title. then ctrl+z on the keyboard to exit the shell.

New to start up with the testing, create a file in the root directory pytest.ini , the file is used to tell pytest where the settings file is located. In the folder add ;

[pytest]

DJANGO_SETTINGS_MODULE = django_pytest.settings

Create tests folder in the blog directory, that is where all your tests files will sit.

Firstly we will test the urls.py file so we’ll create a new folder blog/tests/test_urls.py. Testing the admin path is not required, Django covers that by default. Add the code snippet to the folder just created.

from django.urls import reverse, resolve class TestUrls:



def test_post_content_url(self):

path = reverse('content', kwargs={'pk':1})

assert resolve(path).view_name == "content"

on your command line run

$ python -m pytest

for windows users

$ pytest

You should see something like this.

Now let’s update our views and add login required criterion for accessing the views so we can test for that.

from django.shortcuts import get_object_or_404, render

from .models import Post

from django.contrib.auth.decorators import login_required #new @login_required #new

def post_content(request, pk):

post = get_object_or_404(Post, pk=pk)

return render(request, "post_content.html", {'post':post})

Next, let’s test the views, create a new file for it blog/tests/test_views.py and put;

from django.http import request, response from django.test import RequestFactory from django.urls import reverse from django.contrib.auth.models import User, AnonymousUser from blog.views import post_content from mixer.backend.django import mixer import pytest @pytest.mark.django_db class TestViews:

def test_post_detail_is_authenticated(self): "Test if the user trying to access the view is authenticated" mixer.blend('blog.Post')

path = reverse("content", kwargs={'pk':1})

request = RequestFactory().get(path)

request.user = mixer.blend(User)

response = post_content(request, pk=1)

assert response.status_code == 200 def test_post_detail_is_unauthenticated(self): "Test if the user trying to access the view is unauthenticated"

mixer.blend('blog.Post')

path = reverse("content", kwargs={'pk':1})

request = RequestFactory().get(path)

request.user = AnonymousUser()

response = post_content(request, pk=1)

assert "accounts/login" in response.url

Then run the tests, all should pass

Let’s test the models, create a new file for it blog/tests/test_models.py, and put;

import pytest

from blog.models import Post @pytest.mark.django_db class TestModels:

def test_post_create(self): post = Post.objects.create(

author="Muhammed Ali",

title="Simple article",

content="This is my content",

) def test_model_display_title(self):

post = mixer.blend(Post, title = "Testing")

assert str(post) == "Testing"

Then run your tests, all should pass.

Another important topic to touch on in coverage, we have to be sure that our project is tested. We can do that by running python -m pytest --cov blog or pytest --cov blog for windows, we are testing the blog app alone. Every file should be 100% since we have successfully tested all the files.

Next, let’s omit some folders that we won’t like to be tested. there is no need to test them. To do that lets go to the main directory and create a file .coveragerc and put the below code.

[run] omit = */tests/*

*/migrations/*

Conclusion

Hope this tutorial was able to help you get stated with using pytest with django, if you’ll like to learn more or you hav questions to ask you can reach me at https://twitter.com/Mvhammedali