فصل ۸: تست، استقرار و پروژه‌ی پایانی

تست: TestCase، Client، pytest-django و factory_boy

تست یعنی جرئت تغییر دادن

پروژه‌ای که تست ندارد بعد از شش ماه «دست‌نزدنی» می‌شود: هر تغییر ممکن است جای دیگری را بشکند و کسی جرئت نمی‌کند نسخه‌ی جنگو را به‌روز کند. تست‌های جنگو روی یک پایگاه داده‌ی موقت اجرا می‌شوند و هر تست داخل یک تراکنش است که در پایان rollback می‌شود؛ پس تست‌ها روی هم اثر نمی‌گذارند.

TestCase و Client داخلی جنگو

# apps/orders/tests/test_views.py
from django.test import TestCase
from django.urls import reverse

from apps.accounts.models import User
from apps.orders.models import Customer, Order


class OrderViewsTests(TestCase):
    @classmethod
    def setUpTestData(cls):                       # یک بار برای کل کلاس
        cls.user = User.objects.create_user(mobile="09120000001", password="pass-12345")
        cls.other = User.objects.create_user(mobile="09120000002", password="pass-12345")
        cls.customer = Customer.objects.create(user=cls.user, full_name="علی رضایی")
        cls.order = Order.objects.create(customer=cls.customer, total=120_000_000)

    def test_login_required(self):
        resp = self.client.get(reverse("orders:list"))
        self.assertRedirects(resp, f"{reverse('login')}?next={reverse('orders:list')}")

    def test_owner_sees_order(self):
        self.client.force_login(self.user)
        resp = self.client.get(self.order.get_absolute_url())
        self.assertContains(resp, self.order.tracking_code)

    def test_other_user_gets_404(self):
        self.client.force_login(self.other)
        resp = self.client.get(self.order.get_absolute_url())
        self.assertEqual(resp.status_code, 404)

    def test_list_query_count(self):
        self.client.force_login(self.user)
        with self.assertNumQueries(4):            # session، user، count، list
            self.client.get(reverse("orders:list"))
python manage.py test apps.orders --parallel

pytest-django و factory_boy

pytest خواناتر است، fixture دارد و خروجی خطایش بهتر است. factory_boy هم ساخت داده‌ی آزمایشی را از تکرار نجات می‌دهد:

# pytest.ini
[pytest]
DJANGO_SETTINGS_MODULE = config.settings
python_files = tests.py test_*.py
addopts = --reuse-db -q
# apps/orders/tests/factories.py
import factory
from apps.accounts.models import User
from apps.orders.models import Customer, Order


class UserFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = User
    mobile = factory.Sequence(lambda n: f"0912{n:07d}")


class CustomerFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Customer
    user = factory.SubFactory(UserFactory)
    full_name = factory.Faker("name", locale="fa_IR")


class OrderFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Order
    customer = factory.SubFactory(CustomerFactory)
    total = 50_000_000

# apps/orders/tests/test_services.py
import pytest
from apps.orders.services import OutOfStock, place_order

@pytest.mark.django_db
def test_out_of_stock_rolls_back(carpet_factory):
    carpet = carpet_factory(stock=1)
    customer = CustomerFactory()
    with pytest.raises(OutOfStock):
        place_order(customer, [{"carpet_id": carpet.pk, "quantity": 2}])
    assert customer.orders.count() == 0

(carpet_factory با ثبت factory در conftest.py از طریق پلاگین pytest-factoryboy یا یک fixture ساده ساخته می‌شود.)

نکته‌هایی که کمتر کسی می‌داند

  • setUpTestData داده را یک بار برای کل کلاس می‌سازد و از Django 3.2 هر تست یک کپی ایزوله از اشیا می‌گیرد؛ چند برابر سریع‌تر از setUp.
  • force_login مراحل هش رمز را رد می‌کند و تست‌ها را محسوس سریع‌تر می‌کند؛ برای سرعت بیشتر در تنظیمات تست MD5PasswordHasher بگذارید.
  • TestCase معمولی on_commit را اجرا نمی‌کند چون تراکنش هرگز commit نمی‌شود؛ با self.captureOnCommitCallbacks(execute=True) آن‌ها را اجرا و بررسی کنید.
  • در pytest-django فیکسچر django_assert_num_queries همان assertNumQueries است و settings فیکسچری است که تنظیمات را فقط برای یک تست تغییر می‌دهد.
  • برای تست Celery، CELERY_TASK_ALWAYS_EAGER = True در تنظیمات تست taskها را هم‌زمان اجرا می‌کند؛ بدون نیاز به Redis در CI.

برای ذخیره‌ی پیشرفت و شرکت در آزمون، وارد شوید — رایگان است.