Test Models with Pytest¶
In this guide we are going to:
Setup our project for testing with
pytest.Create tests for the
CustomUsermodel.Create tests for the
Postmodel.Create tests for the
Commentmodel.Create tests for the
Tagmodel.Execute the test suite.
Pre-requisites¶
Requires the Create Django Models for the Blog API guide to be completed.
Guide code:¶
test-models-with-pytest branch in the GitHub repository.
Setup for Testing with Pytest¶
Create folder for tests /src/tests/ and add an empty /src/tests/__init__.py file to it:
mkdir src/tests
touch src/tests/__init__.py
Create file /src/tests/pytest.ini to configure pytest:
[pytest]
DJANGO_SETTINGS_MODULE = blogapi.settings
testpaths =
src/tests
pythonpath =
src
addopts = -v
markers =
model: mark test as model-related
Configure VSCode for pytest¶
Open the command palete (Ctrl-Shift-P)
Select
Python: Configure Testscommand.Select
pytestas testing framework.Select
srcas folder to search for tests.
Test the CustomUser Model¶
Let’s create Python pacakge for accounts app tests:
mkdir src/tests/accounts
touch src/tests/accounts/__init__.py
And now let’s create a test class to test the CustomUser model with first test.
# /src/tests/accounts/test_models.py
from accounts.models import CustomUser
class TestCustomUser:
def test_should_be_returned_by_get_user_model(self):
user_model = get_user_model()
assert user_model is CustomUser
Execute the tests¶
Execute the tests from the command line:
pytest
================================== test session starts ===================================
platform linux -- Python 3.10.13, pytest-8.3.2, pluggy-1.5.0
django: version: 5.0.7, settings: blogapi.settings (from ini)
rootdir: /workspaces/django-for-apis-cookbook-with-blog
configfile: pytest.ini
testpaths: src/tests
plugins: anyio-4.4.0, cov-5.0.0, django-4.8.0
collected 1 item
src/tests/accounts/test_models.py::TestCustomUser::test_should_be_recognized_as_user_model_by_django_auth PASSED [100%]
=================================== 1 passed in 0.04s ====================================
Exclude model tests¶
In pytest.ini we defined model mark which allows us to selectively include or exclude model tests.
pytest -m "not model"
=============================================== test session starts ================================================
platform linux -- Python 3.10.13, pytest-8.3.2, pluggy-1.5.0
django: version: 5.0.7, settings: blogapi.settings (from ini)
rootdir: /workspaces/django-for-apis-cookbook-with-blog
configfile: pytest.ini
testpaths: src/tests
plugins: anyio-4.4.0, cov-5.0.0, django-4.8.0
collected 1 item / 1 deselected / 0 selected
============================================== 1 deselected in 0.06s ===============================================
More CustomUser tests¶
We are going to create tests that user can be created and deleted.
First let’s create a user fixture which could be used in all project tests:
# src/conftest.py
import pytest
from accounts.models import CustomUser
@pytest.fixture(name="user")
def given_user():
u = CustomUser.objects.create()
return u
And add create delete tests to src/tests/accounts/test_models.py:
# ............
class TestCustomUser:
# ............
@pytest.mark.django_db
def test_should_create_user_instance(self):
# WHEN New user model instance is created
u = CustomUser.objects.create()
# THEN the object is a CustomUser instance
assert isinstance(u, CustomUser)
# AND is persisted in the database
u.refresh_from_db()
@pytest.mark.django_db
def test_should_delete_user_instance(self, user: CustomUser):
# WHEN User is deleted
user.delete()
# THEN the user instance is no longer found in the database
with pytest.raises(CustomUser.DoesNotExist):
user.refresh_from_db()
Running the tests:
================================================ test session starts ================================================
platform linux -- Python 3.10.13, pytest-8.3.2, pluggy-1.5.0 -- /home/codespace/.python/current/bin/python
cachedir: .pytest_cache
django: version: 5.0.7, settings: blogapi.settings (from ini)
rootdir: /workspaces/django-for-apis-cookbook-with-blog
configfile: pytest.ini
testpaths: src/tests
plugins: anyio-4.4.0, cov-5.0.0, django-4.8.0
collected 3 items
src/tests/accounts/test_models.py::TestCustomUser::test_should_create_user_instance PASSED [ 33%]
src/tests/accounts/test_models.py::TestCustomUser::test_should_delete_user_instance PASSED [ 66%]
src/tests/accounts/test_models.py::TestCustomUser::test_should_be_recognized_as_user_model_by_django_auth PASSED [100%]
================================================= 3 passed in 0.23s =================================================
Test the Post Model¶
Create test package for posts app:
mkdir src/tests/posts
touch src/tests/posts/__init__.py
And add a src/tests/posts/test_models.py file to it:
import pytest
from posts.models import Post
pytestmark = pytest.mark.model
class TestPost:
@pytest.mark.django_db
def test_should_create_post_instance(self, user):
# WHEN New post model instance is created
p = Post.objects.create(author=user)
# THEN the object is a Post instance
assert isinstance(p, Post)
# AND is persisted in the database
p.refresh_from_db()
@pytest.mark.django_db
def test_should_delete_post_instance(self, post: Post):
# WHEN Post is deleted
post.delete()
# THEN the post instance is no longer found in the database
with pytest.raises(Post.DoesNotExist):
post.refresh_from_db()
For above test we also created a post fixture in src/tests/conftest.py:
# src/tests/conftest.py
import pytest
from accounts.models import CustomUser
from posts.models import Post
# .......................
@pytest.fixture(name="post")
def given_post(user):
p = Post.objects.create(
author=user,
)
return p
Test the Comment Model¶
Add the tests for the Comment model to src/tests/posts/test_models.py:
# src/tests/posts/test_models.py
import pytest
from posts.models import Comment, Post
pytestmark = pytest.mark.model
# .........................
class TestComment:
@pytest.mark.django_db
def test_should_create_comment_instance(self, post, user):
# WHEN New comment model instance is created
c = Comment.objects.create(post=post, author=user)
# THEN the object is a Comment instance
assert isinstance(c, Comment)
# AND is persisted in the database
c.refresh_from_db()
# AND comment is attached to post
assert c in post.comments.all()
@pytest.mark.django_db
def test_should_delete_comment_instance(self, comment: Comment):
# WHEN Post is deleted
comment.delete()
# THEN the post instance is no longer found in the database
with pytest.raises(Comment.DoesNotExist):
comment.refresh_from_db()
@pytest.mark.django_db
def test_should_delete_comment_when_post_is_deleted(self, post: Post, comment: Comment):
# WHEN post is deleted
post.delete()
# THEN the comment is also deleted
with pytest.raises(Comment.DoesNotExist):
comment.refresh_from_db()
Also add a comment fixture to src/tests/conftest.py:
import pytest
from accounts.models import CustomUser
from posts.models import Comment, Post
# .....................
@pytest.fixture(name="comment")
def given_comment(post, user):
c = Comment.objects.create(
post=post,
author=user,
)
return c
Test the Tag Model¶
Add the tests for the Tag model to src/tests/posts/test_models.py:
# src/tests/posts/test_models.py
import pytest
from posts.models import Comment, Post, PostTag, Tag
pytestmark = pytest.mark.model
# ........................
class TestTag:
@pytest.mark.django_db
def test_should_create_tag_instance(self):
# WHEN New tag model instance is created
t = Tag.objects.create(name="tag")
# THEN the object is a Comment instance
assert isinstance(t, Tag)
# AND is persisted in the database
t.refresh_from_db()
@pytest.mark.django_db
def test_should_delete_tag_instance(self, tag: Tag):
# WHEN Tag is deleted
tag.delete()
# THEN the tag instance is no longer found in the database
with pytest.raises(Tag.DoesNotExist):
tag.refresh_from_db()
@pytest.mark.django_db
def test_should_remove_tag_from_post_when_tag_is_deleted(
self, post_with_tag: Post, tag: Tag
):
# WHEN tag is deleted
tag.delete()
# THEN tag is no longer assigned to post
assert 0 == len(post_with_tag.tags.all())
@pytest.mark.django_db
def test_should_not_delete_tag_when_post_is_deleted(self, post_with_tag: Post, tag: Tag):
# WHEN post is deleted
post_with_tag.delete()
# THEN the tag is still in the database
tag.refresh_from_db()
# AND post link to tag is deleted
assert not PostTag.objects.filter(tag=tag).all()
Add also fixtures to src/tests/conftest.py:
# src/tests/conftest.py
import pytest
from accounts.models import CustomUser
from posts.models import Comment, Post, Tag
# ...................................
@pytest.fixture(name="post_with_tag")
def given_post_with_tag(post, tag):
post.tags.set([tag])
return post
@pytest.fixture(name="tag")
def given_tag(post, user):
t = Tag.objects.create(
name="Tag",
)
return t
Execute the test suite¶
Running pytest from the command line passes all tests:
================================================ test session starts ================================================
platform linux -- Python 3.10.13, pytest-8.3.2, pluggy-1.5.0 -- /home/codespace/.python/current/bin/python
cachedir: .pytest_cache
django: version: 5.0.7, settings: blogapi.settings (from ini)
rootdir: /workspaces/django-for-apis-cookbook-with-blog
configfile: pytest.ini
testpaths: src/tests
plugins: anyio-4.4.0, cov-5.0.0, django-4.8.0
collected 12 items
src/tests/accounts/test_models.py::TestCustomUser::test_should_create_user_instance PASSED [ 8%]
src/tests/accounts/test_models.py::TestCustomUser::test_should_delete_user_instance PASSED [ 16%]
src/tests/posts/test_models.py::TestPost::test_should_create_post_instance PASSED [ 25%]
src/tests/posts/test_models.py::TestPost::test_should_delete_post_instance PASSED [ 33%]
src/tests/posts/test_models.py::TestComment::test_should_create_comment_instance PASSED [ 41%]
src/tests/posts/test_models.py::TestComment::test_should_delete_comment_instance PASSED [ 50%]
src/tests/posts/test_models.py::TestComment::test_should_delete_comment_when_post_is_deleted PASSED [ 58%]
src/tests/posts/test_models.py::TestTag::test_should_create_tag_instance PASSED [ 66%]
src/tests/posts/test_models.py::TestTag::test_should_delete_tag_instance PASSED [ 75%]
src/tests/posts/test_models.py::TestTag::test_should_remove_tag_from_post_when_tag_is_deleted PASSED [ 83%]
src/tests/posts/test_models.py::TestTag::test_should_not_delete_tag_when_post_is_deleted PASSED [ 91%]
src/tests/accounts/test_models.py::TestCustomUser::test_should_be_recognized_as_user_model_by_django_auth PASSED [100%]
================================================ 12 passed in 0.38s =================================================