Flaky test yang dipicu oleh order dependency sering kali berakar dari satu akar masalah: mutasi tidak sengaja pada data struktur bersama (shared mutable state). Dalam ekosistem Python, objek dict bersifat mutable secara default. Ketika sebuah dictionary dijadikan fixture atau konfigurasi global antartes, modifikasi nilai di satu unit test dapat membocorkan state (state leak) ke unit test lainnya tanpa disadari.
Artikel ini membahas penerapan frozendict—mengacu pada konsep perancangan mapping immutable seperti pada proposal PEP 814—untuk mengunci dictionary pengujian, mendeteksi pelanggaran akses tulis saat runtime, dan menghapus kebutuhan teardown boilerplate yang rentan galat.
Anatomi Masalah: State Leak dan Order Dependency
Pertimbangkan arsitektur pengujian di mana konfigurasi default didefinisikan sekali di tingkat modul pengujian atau conftest.py, lalu dibagikan ke beberapa test case via fixture berlingkup luas (session atau module scope) untuk efisiensi alokasi memori.
# conftest.py (Bermasalah)
import pytest
GLOBAL_API_CONFIG = {
"endpoint": "https://api.internal.net/v1",
"timeout": 30,
"retries": 3,
"headers": {"X-Environment": "staging"}
}
@pytest.fixture(scope="session")
def base_config():
return GLOBAL_API_CONFIG
Jika satu pengujian memodifikasi dictionary tersebut secara in-place, pengujian berikutnya akan berjalan dengan konteks data yang telah tercemar.
# test_services.py
def test_custom_timeout_mutation(base_config):
# Mutasi in-place tidak disengaja
base_config["timeout"] = 999
assert base_config["timeout"] == 999
def test_default_timeout(base_config):
# Lolos jika dijalankan mandiri, gagal jika dijalankan setelah test_custom_timeout_mutation
assert base_config["timeout"] == 30, f"Expected 30, got {base_config['timeout']}"
Ketika test suite dijalankan secara acak melalui plugin seperti pytest-random-order, test_default_timeout akan menghasilkan kegagalan sporadis (flaky). Urutan eksekusi menjadi penentu keberhasilan pengujian.
Mekanisme frozendict dan Prinsip PEP 814
Proposal PEP 814 mengusulkan antarmuka mapping immutable standar untuk runtime Python. Tujuannya adalah menyediakan tipe data mapping yang mengimplementasikan collections.abc.Mapping penuh, namun secara eksplisit menolak mutasi seperti __setitem__, __delitem__, pop(), atau update().
Di tingkat implementasi (menggunakan pustaka frozendict pihak ketiga yang kompatibel dengan protokol Python), setiap operasi penulisan dicegah langsung pada level runtime dengan melempar TypeError.
from frozendict import frozendict
immutable_config = frozendict({
"endpoint": "https://api.internal.net/v1",
"timeout": 30
})
# Operasi pembacaan valid (memenuhi collections.abc.Mapping)
assert immutable_config["timeout"] == 30
# Upaya mutasi memicu eror seketika
try:
immutable_config["timeout"] = 10
except TypeError as exc:
# frozendict memblokir assignment
pass
Implementasi Fixture Imutabel pada Pytest
Mengubah payload mock dan konfigurasi dasar menjadi frozendict memungkinkan pendeteksian dini terhadap kode produksi atau fungsi pengujian yang secara keliru mengubah argumen input dictionary alih-alih membuat salinan baru.
Konfigurasi Global dan Payload Mock
Perlu diperhatikan bahwa imutabilitas pada level terluar tidak otomatis mencakup sub-dictionary bersarang (nested dict). Lakukan konversi rekursif atau deklarasi spesifik untuk menjamin struktur data benar-benar terkunci.
# test_fixtures.py
import pytest
from frozendict import frozendict
def freeze_recursive(data):
if isinstance(data, dict):
return frozendict({k: freeze_recursive(v) for k, v in data.items()})
return data
@pytest.fixture(scope="session")
def immutable_api_config():
raw_payload = {
"endpoint": "https://api.internal.net/v1",
"timeout": 30,
"retries": 3,
"headers": {"X-Environment": "staging"}
}
return freeze_recursive(raw_payload)
Strategi Verifikasi: Write Attempt Assertion
Dengan fixture di atas, pengujian dapat langsung memverifikasi bahwa kode layanan mematuhi prinsip fungsi murni (pure functions) tanpa efek samping pada argumen mapping yang diterimanya.
# test_client.py
import pytest
def client_worker(config: dict):
# Contoh implementasi buruk di fungsi internal:
# config["executed"] = True
return f"Calling {config['endpoint']}"
def test_client_worker_does_not_mutate(immutable_api_config):
# Jika client_worker melakukan in-place write, TypeError akan dilempar
result = client_worker(immutable_api_config)
assert "Calling" in result
assert immutable_api_config["timeout"] == 30
Jika client_worker mencoba menulis ke immutable_api_config, runner pytest akan langsung menghentikan pengujian dengan stack trace akurat yang menunjuk baris mutasi spesifik, bukan kegagalan tak jelas pada test case lain beberapa menit kemudian.
Eliminasi Teardown Boilerplate
Solusi tradisional untuk mencegah state leak biasanya mengandalkan teardown manual via block yield pada fixture atau penggunaan deepcopy yang memakan komputasi:
# Pendekatan konvensional: Membutuhkan deepcopy atau cleanup manual
@pytest.fixture
def mutable_config_fixture():
import copy
# Overhead alokasi memori berulang setiap test dijalankan
yield copy.deepcopy(GLOBAL_API_CONFIG)
Dengan mengunci struktur data menggunakan frozendict, siklus teardown menjadi redundan. Fixture dapat mempertahankan scope yang luas (session atau module) secara aman karena sifat imutabelnya menjamin objek berada pada status identik dari awal hingga akhir eksekusi tanpa risiko mutasi antar-test runner.
Trade-off dan Batasan Kompatibilitas
Meskipun frozendict memecahkan isu state leak secara struktural, pertimbangkan batasan berikut sebelum integrasi menyeluruh:
- Protokol Mapping vs Konkret Dict:
frozendictlolos pengecekan tipe berbasis abstraksi (isinstance(obj, collections.abc.Mapping)). Namun, pustaka pihak ketiga yang secara kaku memeriksaisinstance(obj, dict)atau validasi tipe C-extension native berpotensi menolak instancefrozendict. - Performa Instansiasi: Penggunaan
freeze_recursivepada struktur dictionary yang sangat besar (jutaan node) menimbulkan latensi pada fase setup awal test suite. - Nested Mutable Elements: Membungkus dictionary terluar tidak membuat elemen tipe list atau set di dalamnya menjadi imutabel secara otomatis. Objek
listtetap harus dikonversi manual menjadituple, dansetmenjadifrozensetuntuk mencegah mutasi pada level anak.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!