Akar Masalah: Transaksi Database Terisolasi, Cache Tidak
Saat menjalankan suite pengujian menggunakan django.test.TestCase, Django membungkus setiap test method di dalam transaksi database atomik dan melakukan rollback saat test selesai. Mekanisme ini menjamin isolasi data pada layer database relasional tanpa memerlukan re-migrasi skema yang memakan waktu.
Isolasi otomatis ini tidak berlaku untuk layer caching. Jika menggunakan backend in-memory seperti django.core.cache.backends.locmem.LocMemCache, Django menyimpan data cache dalam dictionary global di tingkat modul (variabel internal _caches). Karena test runner mengeksekusi semua test dalam satu proses Python yang sama, state dictionary ini tetap bertahan dari satu test ke test berikutnya. Database di-reset, tetapi cache tidak.
Reproduksi Masalah: Lolos Mandiri, Gagal dalam Test Suite
Kebocoran state cache memicu kondisi test pollution: hasil pengujian bergantung pada urutan eksekusi test (order dependency). Kode di bawah mendemonstrasikan bagaimana fungsi membaca data stale jika test sebelumnya tidak membersihkan cache.
from django.test import TestCase
from django.core.cache import cache
from myapp.services import get_user_profile_status
class UserProfileCacheTests(TestCase):
def test_cache_population(self):
# Test A mengisi cache dengan status suspended
cache.set("user_status:123", "suspended", timeout=60)
status = get_user_profile_status(user_id=123)
self.assertEqual(status, "suspended")
def test_fetch_live_status(self):
# Test B mengharapkan fetch langsung dari database baru (default: active)
# Jika dieksekusi setelah test_cache_population, test ini gagal!
status = get_user_profile_status(user_id=123)
self.assertEqual(status, "active")
Jika test_fetch_live_status dijalankan sendiri via ./manage.py test myapp.tests.UserProfileCacheTests.test_fetch_live_status, test lolos karena cache kosong. Namun saat seluruh suite dijalankan, test_cache_population menulis key user_status:123 yang langsung dibaca oleh test_fetch_live_status, menghasilkan assertion error.
Solusi 1: Pembersihan Deterministik dengan Pytest Fixture
Jika menggunakan framework pytest dengan plugin pytest-django, pendekatan paling efektif adalah menggunakan fixture dengan cakupan fungsi (function-scoped) dan parameter autouse=True. Solusi ini menjamin cache dikosongkan sebelum dan setelah setiap test dieksekusi, termasuk jika aplikasi memiliki beberapa cache backend terkonfigurasi.
# conftest.py
import pytest
from django.core.cache import caches
from django.core.cache.backends.base import BaseCache
@pytest.fixture(autouse=True)
def clear_all_django_caches():
"""Bersihkan seluruh cache terkonfigurasi sebelum dan sesudah test."""
def _clear():
for cache_instance in caches.all():
if isinstance(cache_instance, BaseCache):
cache_instance.clear()
_clear()
yield
_clear()
Mengiterasi caches.all() lebih aman daripada hanya memanggil cache.clear() tunggal (yang hanya merujuk ke cache default), terutama jika proyek memisahkan cache sesi atau cache analytics.
Solusi 2: Isolasi pada Standard unittest / TestCase
Bagi proyek yang mengandalkan test runner bawaan Django (unittest), buat custom base test class. Gunakan method addCleanup di dalam setUp atau override tearDown langsung.
# core/test_utils.py
from django.test import TestCase
from django.core.cache import caches
class CleanCacheTestCase(TestCase):
def setUp(self):
super().setUp()
self._clear_caches()
self.addCleanup(self._clear_caches)
@staticmethod
def _clear_caches():
for cache_instance in caches.all():
cache_instance.clear()
Penggunaan self.addCleanup memastikan pembersihan tetap dieksekusi meskipun test method mengalami Exception atau error di tengah jalan.
Konfigurasi Test Settings: Mengunci LocMemCache
Jangan pernah membiarkan environment testing terhubung ke backend cache eksternal seperti Redis atau Memcached yang digunakan pada production atau staging. Selain risiko polusi antar-developer, koneksi jaringan eksternal memperlambat eksekusi suite.
Tetapkan konfigurasi backend in-memory secara eksplisit di file settings_test.py:
# settings_test.py
from .settings import * # noqa
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "unique-test-cache-string",
}
}
Catatan Kritis: Jika sistem Anda memiliki logika yang tidak boleh menjalankan cache sama sekali selama unit testing, ganti backend dengan
django.core.cache.backends.dummy.DummyCache. Namun jika logika bisnis Anda menguji fungsionalitas caching secara eksplisit (seperti cache hit/miss checking), tetap gunakanLocMemCachedengan fixture isolasi di atas.
Debugging Flaky Test Berurutan
Untuk memverifikasi apakah flaky test disebabkan oleh polusi urutan, gunakan tool pytest-randomly atau flag --reverse bawaan Django runner:
# Menjalankan test suite Django dengan urutan terbalik
./manage.py test --reverse
# Menggunakan pytest-randomly dengan seed tertentu
pytest --randomly-seed=12345
Jika kegagalan muncul hanya pada kombinasi urutan tertentu, periksa semua pemanggilan cache.set, variabel class static, atau thread locals yang tidak di-clear setelah test selesai.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!