We have covered installation and how to configure the Django project for the pytest in the previous blog post. Today we will learn how to write tests with pytest for a Django project with some basic examples.
We are going to write tests for:
- models
- views
- and forms
Project Structure
We are going to test the blog app. We have deleted the default tests.py
in blog app, created a folder named tests. The project structure currently looks like this:
├── blog
│ ├── admin.py
│ ├── apps.py
│ ├── forms.py
│ ├── __init__.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ ├── models.py
│ ├── tests
│ │ ├── __init__.py
│ │ ├── test_forms.py
│ │ ├── test_models.py
│ │ └── test_views.py
│ ├── urls.py
│ └── views.py
├── db.sqlite3
├── manage.py
├── media
├── proj
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── pytest.ini
├── README.md
├── static
├── templates
│ └── blog
│ └── index.html
└── test_settings.py
In the previous post, we have shown how to create pytest.ini
and test_settings.py
.
From the filenames in tests, you may assume what’s their purpose. test_forms.py
, test_models.py
, and test_views.py
will contain tests for forms, models, and views respectively. This is the preferred structure for tests.
Let’s get into testing …
Testing Model
So at first, we would like to start with models.py
. We have a model BlogPost in the blog app.
import datetime
from django.db import models
from django.utils import timezone
class BlogPost(models.Model):
title = models.CharField(max_length=120)
slug = models.SlugField(unique=True)
content = models.TextField(null=True, blank=True)
publish_date = models.DateTimeField(auto_now=False, auto_now_add=False, null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
modified_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ('-publish_date', )
def published_recently(self):
now = timezone.now()
two_days_ago = now - datetime.timedelta(days=2)
return two_days_ago < self.publish_date <= now
def __str__(self):
return self.title
Now, we are going to test the functionality of the method published_recently of the BlogPost model whether it’s serving its purpose or not. published_recently checks a BlogPost instance is published recently based on the publish_date field. If publish_date is within 2 days from the current time then it returns True otherwise False.
import datetime
import pytest
from django.utils import timezone
from blog.models import BlogPost
@pytest.mark.django_db
def test_blog_post_published_recently_with_future_post():
publish_date = timezone.now() + datetime.timedelta(days=30)
post = BlogPost.objects.create(
title="Test blog post",
slug="test-blog-post",
content="Test content for blog post.",
publish_date=publish_date
)
assert post.published_recently() is False
@pytest.mark.django_db
def test_blog_post_published_recently_with_old_post():
publish_date = timezone.now() - datetime.timedelta(days=7)
post = BlogPost.objects.create(
title="Test blog post",
slug="test-blog-post",
content="Test content for blog post.",
publish_date=publish_date
)
assert post.published_recently() is False
@pytest.mark.django_db
def test_blog_post_published_recently_with_recent_post():
publish_date = timezone.now()
post = BlogPost.objects.create(
title="Test blog post",
slug="test-blog-post",
content="Test content for blog post.",
publish_date=publish_date
)
assert post.published_recently() is True
As you can see, we are using @pytest.mark.django_db, which is a decorator that helps pytest-django to get access to the test database. Here we have written three tests to test three scenarios that can happen. The first one checks the method returns False for a future blog post. The second test checks for a post published 7 days ago the method returns False. And the last one check for a currently published blog post the method returns True
Testing View
In the blog app, we have a single view index, which renders template index.html with the first three blog posts.
from django.shortcuts import render
from .models import BlogPost
def index(request):
latest_post_list = BlogPost.objects.all()[:3]
context = {"latest_post_list": latest_post_list}
return render(request, "blog/index.html", context)
To test the index view, we have test_index_view function. The test creates two blog posts, gets the response of view using client from pytest. Then it checks if status_code is equal to 200 or not, and checks the two blog posts are in the response’s content.
import pytest
from django.urls import reverse
from django.utils import timezone
from blog.models import BlogPost
from blog.views import index
@pytest.mark.django_db
def test_index_view(client):
publish_date = timezone.now()
BlogPost.objects.create(
title="Test blog post 1",
slug="test-blog-post-1",
content="Test content for blog post 1.",
publish_date=publish_date
)
BlogPost.objects.create(
title="Test blog post 2",
slug="test-blog-post-2",
content="Test content for blog post 2.",
publish_date=publish_date
)
url = reverse("blog:home")
request = client.get(url)
response = index(request)
content = response.content.decode(response.charset)
assert response.status_code == 200
assert "Test blog post 1" in content
assert "Test blog post 2" in content
Testing Form
BlogPostForm, a django model form for the model BlogPost. The form is responsible to take and validate user inputs. It raises a validation error if the user submits a blog post with a title that is already used in another blog post.
from django import forms
from .models import BlogPost
class BlogPostForm(forms.ModelForm):
class Meta:
model = BlogPost
fields = ["title", "slug", "content", "publish_date"]
def clean_title(self, *args, **kwargs):
instance = self.instance
title = self.cleaned_data.get("title")
qs = BlogPost.objects.filter(title__iexact=title)
if instance is not None:
qs = qs.exclude(id=instance.id)
if qs.exists():
raise forms.ValidationError("This title has already been used. Please try again.")
return title
So we have written two tests for the BlogPostForm. The first one check passes valid form data and checks the form is valid. and the second one checks invalid form, at first we create a blog post and in the form data, we pass the exact same title of that blog post. The test checks the form is invalid and makes sure the form shows the right validation error.
import pytest
from django.utils import timezone
from blog.forms import BlogPostForm
from blog.models import BlogPost
@pytest.mark.django_db
def test_blog_post_form_valid():
publish_date = timezone.now()
form_data = {
"title": "Test blog post",
"slug": "test-blog-post",
"content": "Test content for blog post.",
"publish_date": publish_date,
}
form = BlogPostForm(data=form_data)
assert form.is_valid() is True
@pytest.mark.django_db
def test_blog_post_form_invalid():
publish_date = timezone.now()
BlogPost.objects.create(
title="Test blog post",
slug="test-blog-post",
content="Test content for blog post.",
publish_date=publish_date
)
form_data = {
"title": "Test blog post",
"slug": "test-blog-post",
"content": "Test content for blog post.",
"publish_date": publish_date,
}
form = BlogPostForm(data=form_data)
assert form.is_valid() is False
assert "This title has already been used. Please try again." == form.errors["title"][0]
Now it’s time to run the tests we have written so far!
pytest
=============================================================================================== test session starts ===============================================================================================
platform linux -- Python 3.6.9, pytest-5.3.2, py-1.8.1, pluggy-0.13.1
Django settings: test_settings (from ini file)
rootdir: /home/adnan/Blog_Post_Snippets/blog_project/blog_proj, inifile: pytest.ini
plugins: django-3.7.0
collected 6 items
blog/tests/test_forms.py .. [ 33%]
blog/tests/test_models.py ... [ 83%]
blog/tests/test_views.py . [100%]
================================================================================================ 6 passed in 0.23s ================================================================================================
Tests passed!
The source code of the project is available on GitHub: https://github.com/dreamcatcherit/django_pytest_blog_project
I hope this post helped you to give a basic overview of the possibilities of the pytest . See you next time!
Author:
Adnan Alam
Self-taught programmer | Pythonista | Love to solve problems!
Dreamcatcher IT
Github | Website | Linkedin