Files
mostovik-backend/tests/apps/core/test_bulk_operations.py
Aleksandr Meshchriakov 0f17ff6773
All checks were successful
CI/CD Pipeline / Quality Gate (push) Successful in 26s
CI/CD Pipeline / Build and Push Images (push) Successful in 6s
CI/CD Pipeline / Internal Notify (push) Successful in 0s
CI/CD Pipeline / Deploy Dev in Dokploy (push) Successful in 1s
Add organizations v2 API and registry enrichment
2026-05-06 19:04:46 +02:00

268 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Тесты для BulkOperationsMixin и QueryOptimizerMixin."""
from unittest.mock import patch
from apps.core.models import BackgroundJob
from apps.core.services import (
BulkOperationsMixin,
QueryOptimizerMixin,
)
from django.contrib.auth import get_user_model
from django.db.models import OuterRef
from django.test import TestCase
from faker import Faker
fake = Faker()
class BulkOperationsMixinTest(TestCase):
"""Тесты для BulkOperationsMixin."""
def test_mixin_has_bulk_create_chunked(self):
"""Проверка наличия метода bulk_create_chunked."""
self.assertTrue(hasattr(BulkOperationsMixin, "bulk_create_chunked"))
def test_mixin_has_bulk_update_or_create(self):
"""Проверка наличия метода bulk_update_or_create."""
self.assertTrue(hasattr(BulkOperationsMixin, "bulk_update_or_create"))
def test_mixin_has_bulk_delete(self):
"""Проверка наличия метода bulk_delete."""
self.assertTrue(hasattr(BulkOperationsMixin, "bulk_delete"))
def test_mixin_has_bulk_update_fields(self):
"""Проверка наличия метода bulk_update_fields."""
self.assertTrue(hasattr(BulkOperationsMixin, "bulk_update_fields"))
class QueryOptimizerMixinTest(TestCase):
"""Тесты для QueryOptimizerMixin."""
def test_mixin_has_get_optimized_queryset(self):
"""Проверка наличия метода get_optimized_queryset."""
self.assertTrue(hasattr(QueryOptimizerMixin, "get_optimized_queryset"))
def test_mixin_has_apply_optimizations(self):
"""Проверка наличия метода apply_optimizations."""
self.assertTrue(hasattr(QueryOptimizerMixin, "apply_optimizations"))
def test_mixin_has_get_list_queryset(self):
"""Проверка наличия метода get_list_queryset."""
self.assertTrue(hasattr(QueryOptimizerMixin, "get_list_queryset"))
def test_mixin_has_get_detail_queryset(self):
"""Проверка наличия метода get_detail_queryset."""
self.assertTrue(hasattr(QueryOptimizerMixin, "get_detail_queryset"))
def test_mixin_has_with_counts(self):
"""Проверка наличия метода with_counts."""
self.assertTrue(hasattr(QueryOptimizerMixin, "with_counts"))
def test_mixin_has_with_exists(self):
"""Проверка наличия метода with_exists."""
self.assertTrue(hasattr(QueryOptimizerMixin, "with_exists"))
def test_mixin_default_attributes(self):
"""Проверка атрибутов по умолчанию."""
self.assertEqual(QueryOptimizerMixin.select_related, [])
self.assertEqual(QueryOptimizerMixin.prefetch_related, [])
self.assertEqual(QueryOptimizerMixin.default_only, [])
self.assertEqual(QueryOptimizerMixin.default_defer, [])
def test_optimizer_methods_execute(self):
User = get_user_model()
class UserService(QueryOptimizerMixin):
model = User
prefetch_related = ["groups"]
default_only = ["id", "email"]
default_defer = ["password"]
qs = UserService.get_optimized_queryset()
self.assertIsNotNone(qs)
qs_no_opts = UserService.apply_optimizations(
User.objects.all(),
include_select=False,
include_prefetch=False,
include_only=False,
include_defer=False,
)
self.assertIsNotNone(qs_no_opts)
qs_list = UserService.get_list_queryset()
qs_detail = UserService.get_detail_queryset()
self.assertIsNotNone(qs_list)
self.assertIsNotNone(qs_detail)
qs_counts = UserService.with_counts(User.objects.all(), "groups")
self.assertIsNotNone(qs_counts)
qs_exists = UserService.with_exists(
User.objects.all(),
has_self=User.objects.filter(pk=OuterRef("pk")),
)
self.assertIsNotNone(qs_exists)
class BulkOperationsIntegrationTest(TestCase):
"""Интеграционные тесты для bulk операций с BackgroundJob."""
def test_bulk_create_chunked(self):
"""Тест массового создания чанками."""
# Создаём тестовый сервис с BulkOperationsMixin
class TestService(BulkOperationsMixin):
model = BackgroundJob
# Создаём 10 объектов чанками по 3
jobs = [
BackgroundJob(
task_id=f"bulk-chunk-{i}",
task_name="test.bulk.task",
)
for i in range(10)
]
count = TestService.bulk_create_chunked(jobs, chunk_size=3)
self.assertEqual(count, 10)
# Проверяем что все созданы
self.assertEqual(
BackgroundJob.objects.filter(task_name="test.bulk.task").count(), 10
)
def test_bulk_delete(self):
"""Тест массового удаления."""
class TestService(BulkOperationsMixin):
model = BackgroundJob
# Создаём несколько задач
jobs = []
for i in range(5):
job = BackgroundJob.objects.create(
task_id=f"bulk-delete-{i}",
task_name="test.delete.task",
)
jobs.append(job)
# Удаляем первые 3
ids_to_delete = [j.pk for j in jobs[:3]]
deleted = TestService.bulk_delete(ids_to_delete)
self.assertEqual(deleted, 3)
self.assertEqual(
BackgroundJob.objects.filter(task_name="test.delete.task").count(), 2
)
def test_bulk_update_fields(self):
"""Тест массового обновления полей."""
class TestService(BulkOperationsMixin):
model = BackgroundJob
# Создаём задачи
for i in range(5):
BackgroundJob.objects.create(
task_id=f"bulk-update-{i}",
task_name="test.update.task",
progress=0,
)
# Обновляем все задачи этого типа
updated = TestService.bulk_update_fields(
filters={"task_name": "test.update.task"},
updates={"progress": 50},
)
self.assertEqual(updated, 5)
# Проверяем что обновились
for job in BackgroundJob.objects.filter(task_name="test.update.task"):
self.assertEqual(job.progress, 50)
def test_bulk_update_or_create_creates(self):
"""Тест upsert - создание новых."""
class TestService(BulkOperationsMixin):
model = BackgroundJob
items = [
{"task_id": "upsert-new-1", "task_name": "upsert.task", "progress": 10},
{"task_id": "upsert-new-2", "task_name": "upsert.task", "progress": 20},
]
created, updated = TestService.bulk_update_or_create(
items=items,
unique_fields=["task_id"],
update_fields=["task_name", "progress"],
)
self.assertEqual(created, 2)
self.assertEqual(updated, 0)
def test_bulk_update_or_create_updates(self):
"""Тест upsert - обновление существующих."""
class TestService(BulkOperationsMixin):
model = BackgroundJob
# Создаём существующую запись
BackgroundJob.objects.create(
task_id="upsert-existing",
task_name="old.task",
progress=0,
)
items = [
{"task_id": "upsert-existing", "task_name": "new.task", "progress": 100},
]
created, updated = TestService.bulk_update_or_create(
items=items,
unique_fields=["task_id"],
update_fields=["task_name", "progress"],
)
self.assertEqual(created, 0)
self.assertEqual(updated, 1)
# Проверяем обновление
job = BackgroundJob.objects.get(task_id="upsert-existing")
self.assertEqual(job.task_name, "new.task")
self.assertEqual(job.progress, 100)
def test_bulk_update_or_create_does_not_use_per_row_update_or_create(self):
"""Upsert должен идти через bulk операции, а не update_or_create в цикле."""
class TestService(BulkOperationsMixin):
model = BackgroundJob
BackgroundJob.objects.create(
task_id="upsert-bulk-existing",
task_name="old.task",
progress=0,
)
items = [
{
"task_id": "upsert-bulk-existing",
"task_name": "new.task",
"progress": 100,
},
{"task_id": "upsert-bulk-new", "task_name": "new.task", "progress": 50},
]
with patch(
"django.db.models.query.QuerySet.update_or_create",
side_effect=AssertionError("per-row update_or_create must not be used"),
):
created, updated = TestService.bulk_update_or_create(
items=items,
unique_fields=["task_id"],
update_fields=["task_name", "progress"],
)
self.assertEqual(created, 1)
self.assertEqual(updated, 1)