TestCase, Client, RequestFactory, pytest-django, fixtures, factories
Тестирование — критическая часть разработки надёжных Django приложений. Этот туториал охватывает unit тестирование моделей, форм и view.
💡 Правило: Пишите тесты для критической бизнес-логики. Не тестируйте очевидные вещи (например, что save() сохраняет). Тестируйте поведение, а не реализацию.
# settings.py
# Тестовые настройки
TESTING = True
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:', # In-memory БД для скорости
}
}
# Или для PostgreSQL
# DATABASES = {
# 'default': {
# 'ENGINE': 'django.db.backends.postgresql',
# 'NAME': 'test_db',
# 'USER': 'test',
# 'PASSWORD': 'test',
# }
# }blog/
├── __init__.py
├── models.py
├── views.py
├── forms.py
├── tests/
│ ├── __init__.py
│ ├── test_models.py
│ ├── test_views.py
│ ├── test_forms.py
│ └── test_utils.py
└── tests.py # Для простых проектов
# blog/tests/test_models.py
from django.test import TestCase
from django.contrib.auth import get_user_model
from blog.models import Post, Category
User = get_user_model()
class PostModelTest(TestCase):
"""Тесты для модели Post."""
@classmethod
def setUpTestData(cls):
"""Вызывается один раз перед всеми тестами."""
cls.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpass123'
)
cls.category = Category.objects.create(name='Test Category')
def setUp(self):
"""Вызывается перед каждым тестом."""
self.post = Post.objects.create(
title='Test Post',
content='Test content',
author=self.user,
category=self.category
)
def test_post_creation(self):
"""Тест создания поста."""
self.assertIsNotNone(self.post.pk)
self.assertEqual(self.post.title, 'Test Post')
self.assertEqual(self.post.author, self.user)
def test_post_str(self):
"""Тест строкового представления."""
self.assertEqual(str(self.post), 'Test Post')
def test_post_slug_auto_generation(self):
"""Тест автогенерации slug."""
self.assertEqual(self.post.slug, 'test-post')
def test_post_absolute_url(self):
"""Тест get_absolute_url."""
self.assertEqual(self.post.get_absolute_url(), f'/posts/{self.post.pk}/')
def test_post_published_status(self):
"""Тест статуса публикации."""
self.assertFalse(self.post.is_published)
self.post.is_published = True
self.post.save()
self.assertTrue(self.post.is_published)# Запустить все тесты
python manage.py test
# Запустить тесты приложения
python manage.py test blog
# Запустить конкретный файл
python manage.py test blog.tests.test_models
# Запустить конкретный тест
python manage.py test blog.tests.test_models.PostModelTest.test_post_creation
# С verbosity для подробного вывода
python manage.py test blog --verbosity=2
# С coverage для измерения покрытия
coverage run manage.py test blog
coverage report
coverage html # HTML отчёт# blog/tests/test_views.py
from django.test import TestCase
from django.urls import reverse
from django.contrib.auth import get_user_model
from blog.models import Post
User = get_user_model()
class PostViewTest(TestCase):
"""Тесты для view постов."""
@classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
cls.post = Post.objects.create(
title='Test Post',
content='Test content',
author=cls.user,
is_published=True
)
def test_post_list_view(self):
"""Тест списка постов."""
response = self.client.get(reverse('post_list'))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'blog/post_list.html')
self.assertContains(response, 'Test Post')
self.assertIn('posts', response.context)
self.assertEqual(len(response.context['posts']), 1)
def test_post_detail_view(self):
"""Тест детального просмотра."""
response = self.client.get(reverse('post_detail', kwargs={'pk': self.post.pk}))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'blog/post_detail.html')
self.assertContains(response, 'Test Post')
self.assertEqual(response.context['post'], self.post)
def test_post_detail_not_found(self):
"""Тест 404 для несуществующего поста."""
response = self.client.get(reverse('post_detail', kwargs={'pk': 999}))
self.assertEqual(response.status_code, 404)
def test_post_create_view_authenticated(self):
"""Тест создания поста авторизованным пользователем."""
self.client.login(username='testuser', password='testpass123')
response = self.client.post(reverse('post_create'), {
'title': 'New Post',
'content': 'New content',
'category': self.post.category.pk,
})
self.assertEqual(response.status_code, 302) # Redirect после создания
self.assertTrue(Post.objects.filter(title='New Post').exists())
def test_post_create_view_unauthenticated(self):
"""Тест что неавторизованный не может создать пост."""
response = self.client.post(reverse('post_create'), {
'title': 'New Post',
'content': 'New content',
})
self.assertEqual(response.status_code, 302) # Redirect на login
self.assertFalse(Post.objects.filter(title='New Post').exists())from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth import get_user_model
from blog.models import Post
User = get_user_model()
class PostCBVTest(TestCase):
"""Тесты для Class-Based Views."""
def setUp(self):
self.client = Client()
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.post = Post.objects.create(
title='Test Post',
content='Test content',
author=self.user,
is_published=True
)
def test_list_view_pagination(self):
"""Тест пагинации в ListView."""
# Создать 25 постов (при paginate_by=10)
for i in range(24):
Post.objects.create(
title=f'Post {i}',
content='Content',
author=self.user,
is_published=True
)
response = self.client.get(reverse('post_list'))
self.assertEqual(response.status_code, 200)
self.assertTrue(response.context['is_paginated'])
self.assertEqual(len(response.context['object_list']), 10) # paginate_by
def test_update_view(self):
"""Тест UpdateView."""
self.client.login(username='testuser', password='testpass123')
response = self.client.post(reverse('post_update', kwargs={'pk': self.post.pk}), {
'title': 'Updated Title',
'content': 'Updated content',
'category': self.post.category.pk,
})
self.assertEqual(response.status_code, 302)
self.post.refresh_from_db()
self.assertEqual(self.post.title, 'Updated Title')
def test_delete_view(self):
"""Тест DeleteView."""
self.client.login(username='testuser', password='testpass123')
response = self.client.post(reverse('post_delete', kwargs={'pk': self.post.pk}))
self.assertEqual(response.status_code, 302)
self.assertFalse(Post.objects.filter(pk=self.post.pk).exists())from django.test import RequestFactory, TestCase
from django.contrib.auth import get_user_model
from blog.views import post_detail, post_create
from blog.models import Post
User = get_user_model()
class PostViewUnitTest(TestCase):
"""Unit тесты view функций с RequestFactory."""
def setUp(self):
self.factory = RequestFactory()
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
def test_post_detail_view(self):
"""Тест post_detail view."""
post = Post.objects.create(
title='Test Post',
content='Test content',
author=self.user,
is_published=True
)
# Создать request
request = self.factory.get(f'/posts/{post.pk}/')
request.user = self.user # Установить пользователя
# Вызвать view
response = post_detail(request, pk=post.pk)
# Проверки
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context_data['post'], post)
def test_post_create_view_post(self):
"""Тест post_create view с POST данными."""
request = self.factory.post('/posts/create/', {
'title': 'New Post',
'content': 'New content',
'category': 1,
})
request.user = self.user
response = post_create(request)
self.assertEqual(response.status_code, 302) # Redirect
self.assertTrue(Post.objects.filter(title='New Post').exists())from django.test import RequestFactory, TestCase, override_settings
from django.contrib.sessions.middleware import SessionMiddleware
from django.contrib.auth.middleware import AuthenticationMiddleware
def add_session_to_request(request):
"""Добавить сессию к request."""
middleware = SessionMiddleware(lambda req: None)
middleware.process_request(request)
request.session.save()
def add_user_to_request(request, user=None):
"""Добавить пользователя к request."""
add_session_to_request(request)
middleware = AuthenticationMiddleware(lambda req: None)
middleware.process_request(request)
request.user = user or AnonymousUser()
class ViewWithMiddlewareTest(TestCase):
"""Тесты с middleware."""
def setUp(self):
self.factory = RequestFactory()
self.user = User.objects.create_user('testuser', 'test@test.com', 'pass')
def test_view_with_session(self):
request = self.factory.get('/some-url/')
add_session_to_request(request)
add_user_to_request(request, self.user)
# Теперь request.session и request.user доступны
request.session['key'] = 'value'
self.assertEqual(request.user, self.user)# blog/tests/test_forms.py
from django.test import TestCase
from blog.forms import PostForm, CommentForm
from blog.models import Post, Category
class PostFormTest(TestCase):
"""Тесты для PostForm."""
@classmethod
def setUpTestData(cls):
cls.category = Category.objects.create(name='Test')
def test_valid_form(self):
"""Тест валидной формы."""
form = PostForm(data={
'title': 'Test Post',
'content': 'Test content',
'category': self.category.pk,
})
self.assertTrue(form.is_valid())
def test_invalid_form_missing_title(self):
"""Тест формы без заголовка."""
form = PostForm(data={
'content': 'Test content',
'category': self.category.pk,
})
self.assertFalse(form.is_valid())
self.assertIn('title', form.errors)
def test_invalid_form_short_title(self):
"""Тест формы с коротким заголовком."""
form = PostForm(data={
'title': 'AB', # Минимум 3 символа
'content': 'Test content',
'category': self.category.pk,
})
self.assertFalse(form.is_valid())
self.assertIn('title', form.errors)
def test_form_save(self):
"""Тест сохранения формы."""
form = PostForm(data={
'title': 'Test Post',
'content': 'Test content',
'category': self.category.pk,
})
post = form.save()
self.assertIsNotNone(post.pk)
self.assertEqual(post.title, 'Test Post')class CommentFormTest(TestCase):
"""Тесты для CommentForm с кастомной валидацией."""
def test_clean_content_too_short(self):
"""Тест валидации длины комментария."""
form = CommentForm(data={'content': 'Hi'}) # Минимум 10 символов
self.assertFalse(form.is_valid())
self.assertIn('content', form.errors)
def test_clean_content_spam_words(self):
"""Тест валидации на спам."""
form = CommentForm(data={
'content': 'Buy cheap products at spam.com'
})
self.assertFalse(form.is_valid())
self.assertIn('content', form.errors)
def test_clean_duplicate_comment(self):
"""Тест на дубликат комментария."""
user = User.objects.create_user('testuser', 'test@test.com', 'pass')
post = Post.objects.create(title='Test', content='Content', author=user)
Comment.objects.create(
post=post,
author=user,
content='Duplicate comment'
)
form = CommentForm(data={
'post': post.pk,
'author': user,
'content': 'Duplicate comment'
})
self.assertFalse(form.is_valid())# fixtures/users.json
[
{
"model": "auth.user",
"pk": 1,
"fields": {
"username": "testuser",
"email": "test@example.com",
"password": "pbkdf2_sha256$..."
}
}
]
# blog/tests/test_with_fixtures.py
class PostFixtureTest(TestCase):
fixtures = ['users.json', 'posts.json']
def test_posts_loaded(self):
"""Тест что fixtures загрузились."""
self.assertEqual(Post.objects.count(), 5)
self.assertEqual(User.objects.count(), 1)pip install model_bakery# blog/tests/test_with_bakery.py
from model_bakery import baker
from django.test import TestCase
from blog.models import Post, Comment
class PostBakeryTest(TestCase):
"""Тесты с model_bakery."""
def test_post_creation(self):
"""Тест создания поста через baker."""
post = baker.make(Post, title='Test Post')
self.assertEqual(post.title, 'Test Post')
def test_post_with_relations(self):
"""Тест поста со связями."""
post = baker.make(Post, _fill_optional=True)
self.assertIsNotNone(post.author)
self.assertIsNotNone(post.category)
def test_multiple_posts(self):
"""Тест создания нескольких постов."""
posts = baker.make(Post, _quantity=10)
self.assertEqual(len(posts), 10)
def test_post_with_comments(self):
"""Тест поста с комментариями."""
post = baker.make(Post)
baker.make(Comment, _quantity=5, post=post)
self.assertEqual(post.comments.count(), 5)# blog/tests/test_integration.py
from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth import get_user_model
from model_bakery import baker
from blog.models import Post, Category, Comment
User = get_user_model()
class BlogIntegrationTest(TestCase):
"""Integration тесты для блога."""
def setUp(self):
self.client = Client()
self.user = User.objects.create_user(
username='author',
password='pass123'
)
self.category = Category.objects.create(name='Django')
def test_full_post_workflow(self):
"""Тест полного цикла работы с постом."""
# Login
self.client.login(username='author', password='pass123')
# Create
create_url = reverse('post_create')
response = self.client.post(create_url, {
'title': 'Integration Test Post',
'content': 'Test content for integration',
'category': self.category.pk,
}, follow=True)
self.assertEqual(response.status_code, 200)
post = Post.objects.get(title='Integration Test Post')
# View
detail_url = reverse('post_detail', kwargs={'pk': post.pk})
response = self.client.get(detail_url)
self.assertContains(response, 'Integration Test Post')
# Update
update_url = reverse('post_update', kwargs={'pk': post.pk})
response = self.client.post(update_url, {
'title': 'Updated Integration Test Post',
'content': 'Updated content',
'category': self.category.pk,
}, follow=True)
post.refresh_from_db()
self.assertEqual(post.title, 'Updated Integration Test Post')
# Add Comment
comment_url = reverse('add_comment', kwargs={'pk': post.pk})
response = self.client.post(comment_url, {
'content': 'Great post!',
}, follow=True)
self.assertEqual(post.comments.count(), 1)
# Delete
delete_url = reverse('post_delete', kwargs={'pk': post.pk})
response = self.client.post(delete_url, follow=True)
self.assertFalse(Post.objects.filter(pk=post.pk).exists())
def test_post_search(self):
"""Тест поиска постов."""
baker.make(Post, title='Django Tutorial', content='Learn Django', is_published=True, _quantity=3)
baker.make(Post, title='Python Basics', content='Learn Python', is_published=True, _quantity=2)
search_url = reverse('post_search')
response = self.client.get(search_url, {'q': 'Django'})
self.assertEqual(response.context['posts'].count(), 3)Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.