Inisialisasi test runner bawaan Django pada Continuous Integration (CI) sering menjadi bottleneck utama. Menjalankan migrasi database dari nol secara sekuensial pada setiap commit membuang resource komputasi dan memperlambat feedback loop engineering. Dengan mengombinasikan pytest-django dan pytest-xdist, eksekusi test suite dapat diparalelkan secara horizontal di level worker proses dengan isolasi database penuh.
1. Bottleneck Inisialisasi Database dan Eksekusi Sekuensial
Secara default, runner manage.py test mengeksekusi test case secara serial dalam satu proses. Jika project memiliki ratusan file migrasi dan ribuan skenario test, dua masalah utama muncul:
- Overhead Migrasi Berulang: Runner mengaplikasikan seluruh migration graph ke database pengujian sementara pada setiap job runner baru.
- Underutilization Hardware: Runner CI modern (seperti GitHub Actions runner dengan 2 atau 4 vCPU) hanya menggunakan 1 core, menyisakan core lain menganggur sementara IO dan CPU terkunci pada satu thread.
Solusinya adalah membagi eksekusi test ke beberapa proses worker independen menggunakan pytest-xdist dan mengisolasi state database untuk tiap worker menggunakan fitur sub-database dari pytest-django.
2. Arsitektur Isolasi Database PostgreSQL Per-Worker
Saat menjalankan parallel testing dengan PostgreSQL, setiap worker proses membutuhkan skema atau database terpisah agar operasi DDL/DML tidak saling mengunci (deadlock) atau menimbulkan data leakage.
Ketika flag -n <workers> diaktifkan, pytest-xdist membuat worker dengan identitas gw0, gw1, dan seterusnya. pytest-django memanfaatkan metadata ini untuk membuat database terpisah dengan format penamaan:
test_<nama_database_asli>_<worker_id>Contoh: jika nama database utama adalah app_db, worker 0 akan mengakses test_app_db_gw0 dan worker 1 akan mengakses test_app_db_gw1.
Prasyarat Hak Akses PostgreSQL
User database pada environment CI harus memiliki hak istimewa CREATEDB agar worker dapat membuat database turunannya masing-masing secara otomatis saat inisialisasi awal:
ALTER USER ci_user CREATEDB;Manajemen Flag --reuse-db dan --create-db
Flag --reuse-db mencegah pytest membuat ulang dan mengeksekusi migrasi jika database test dari run sebelumnya masih tersedia. Ini memangkas waktu startup dari puluhan detik menjadi seketika.
- Gunakan
--reuse-db: Untuk eksekusi reguler ketika tidak ada perubahan skema migrasi. - Gunakan
--create-db: Untuk memaksa pytest mereset dan memigrasi ulang database ketika file migrasi baru ditambahkan.
3. Konfigurasi pytest.ini dan Penanganan Fixture
Simpan konfigurasi inti test runner di root repository pada file pytest.ini:
[pytest]
DJANGO_SETTINGS_MODULE = core.settings.test
python_files = tests.py test_*.py *_tests.py
addopts =
-n auto
--dist loadscope
--reuse-db
--maxfail=3
-p no:warnings
Strategi distribusi --dist loadscope mengelompokkan test berdasarkan modul/kelas agar dieksekusi pada worker yang sama, menjaga kohesi pemanggilan resource.
Isolasi Transaksi: db vs transactional_db
Pemilihan fixture database menentukan kecepatan dan risiko kontaminasi data antar-test:
db: Membungkus setiap test case ke dalam satu transaksi atomik (atomic transaction) dan melakukan rollback saat test selesai. Sangat cepat, aman dari state pollution, dan cukup untuk 95% kebutuhan test.transactional_db: Menjalankan komit langsung dan membersihkan data viaTRUNCATEtable setelah test selesai. Gunakan ini hanya jika menguji interaksi multi-threaded, task background (seperti Celery dengantransaction.on_commit), atau pengujian transaksi bertingkat.
Implementasi fixture aman pada conftest.py:
import pytest
from rest_framework.test import APIClient
@pytest.fixture
def api_client(db):
"""Client terisolasi menggunakan transaksi rollback."""
return APIClient()
4. Mitigasi Race Condition: Shared Storage & Redis Cache
Membagi eksekusi ke beberapa worker berpotensi menimbulkan race condition jika kode mengakses storage lokal atau in-memory cache bersama.
Isolasi Media File (Shared Storage)
Jika test suite menguji file upload, path media lokal tidak boleh menimpa file antar-worker. Alihkan MEDIA_ROOT ke temporary folder unik per-worker via hook conftest.py:
import os
import pytest
from django.conf import settings
@pytest.fixture(autouse=True)
def isolate_media_storage(tmp_path_factory):
worker_id = os.environ.get("PYTEST_XDIST_WORKER", "master")
worker_temp_dir = tmp_path_factory.mktemp(f"media_{worker_id}")
settings.MEDIA_ROOT = str(worker_temp_dir)
Isolasi Redis Cache
Jika menggunakan backend cache terpusat (Redis), operasi cache.clear() pada satu worker akan menghapus cache worker lain secara tidak terduga. Terapkan salah satu strategi berikut:
- Ganti ke Dummy/LocMem: Arahkan cache ke
django.core.cache.backends.locmem.LocMemCachedi settings pengujian. Tiap worker proses secara natural memiliki memory space masing-masing. - Dynamic Key Prefix: Jika pengujian wajib terhubung ke Redis nyata, modifikasi prefix cache berdasarkan worker:
worker_id = os.environ.get("PYTEST_XDIST_WORKER", "single")
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1",
"KEY_PREFIX": f"test_cache_{worker_id}",
}
}
5. Implementasi Pipeline GitHub Actions
Berikut implementasi lengkap pipeline CI GitHub Actions yang memanfaatkan multi-worker, service PostgreSQL, dan caching virtualenv:
name: Django Test Suite
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15-alpine
env:
POSTGRES_DB: app_test_db
POSTGRES_USER: ci_user
POSTGRES_PASSWORD: ci_password
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: "pip"
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest-django pytest-xdist
- name: Check Migrations Status
env:
DATABASE_URL: postgres://ci_user:ci_password@localhost:5432/app_test_db
run: |
python manage.py makemigrations --check --dry-run
- name: Run Tests with pytest-xdist
env:
DATABASE_URL: postgres://ci_user:ci_password@localhost:5432/app_test_db
DJANGO_SETTINGS_MODULE: core.settings.test
run: |
pytest -n 2 --dist loadscope
Ringkasan Praktik Terbaik
- Hindari shared state pada filesystem dengan selalu mengisolasi
MEDIA_ROOTviatmp_path_factory. - Gunakan isolation engine berbasis rollback (
db) untuk meminimalkan lock dan query truncation tabel. - Batasi worker parallelism sesuai core vCPU runner host (GitHub default runner:
-n 2) untuk mencegah CPU thrashing.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!