Semakin banyak test kita tulis, semakin banyak juga persiapan yang berulang: setiap test keranjang butuh KeranjangBelanja yang segar, setiap test berkas butuh file sementara, setiap test database butuh koneksi bersih. Menyalin-tempel kode persiapan ke puluhan test membuat perubahan desain class menular menjadi perubahan di mana-mana. pytest menawarkan fixtures: fungsi penyedia data yang dijalankan otomatis dan disuntikkan ke test lewat nama parameternya.
Fixture Pertama #
Kita pakai class KeranjangBelanja berikut sebagai objek yang butuh persiapan:
class KeranjangBelanja:
def __init__(self):
self.items = {}
def tambah_item(self, nama, harga, jumlah=1):
if nama in self.items:
self.items[nama]["jumlah"] += jumlah
else:
self.items[nama] = {"harga": harga, "jumlah": jumlah}
def total(self):
return sum(
rincian["harga"] * rincian["jumlah"]
for rincian in self.items.values()
)
def jumlah_item(self):
return sum(rincian["jumlah"] for rincian in self.items.values())Fixture didefinisikan dengan decorator @pytest.fixture. Simpan file conftest.py:
import pytest
from keranjang import KeranjangBelanja
@pytest.fixture
def keranjang():
return KeranjangBelanja()Test menggunakan fixture cukup dengan mendeklarasikan parameter bernama sama:
def test_tambah_satu_item(keranjang):
keranjang.tambah_item("Kopi", 25000)
assert keranjang.jumlah_item() == 1
def test_item_sama_menambah_jumlah(keranjang):
keranjang.tambah_item("Kopi", 25000)
keranjang.tambah_item("Kopi", 25000, 2)
assert keranjang.jumlah_item() == 3
assert keranjang.total() == 75000Mari kita telaah alur yang terjadi:
- Parameter
keranjangpada test: pytest membaca nama parameter, mencari fixture bernamakeranjang, menjalankan fixture tersebut, lalu menyuntikkan nilai baliknya ke test. - File
conftest.py: semua fixture di file ini otomatis tersedia untuk test di folder yang sama dan folder turunannya, tanpa perluimport. - Fixture berjalan per test: setiap test menerima instance
KeranjangBelanjayang baru, sehingga test pertama tidak mencemari data test kedua — pola ini menggantikansetUp()pada unittest.
Output yang Dihasilkan (dipangkas dari run python3 -m pytest -v):
test_keranjang.py::test_tambah_satu_item PASSED [ 25%]
test_keranjang.py::test_item_sama_menambah_jumlah PASSED [ 50%]Mengatur Umur Fixture dengan scope #
Secara bawaan, fixture dibangun dan dibongkar untuk setiap test (scope "function"). Tabel berikut merangkum pilihan scope:
| Scope | Fixture dibuat ulang | Cocok untuk |
|---|---|---|
function (bawaan) | setiap test | objek berstatus seperti KeranjangBelanja |
class | setiap class test | data mahal yang boleh dibagi antar method dalam satu class |
module | setiap file test | koneksi baca-saja, konfigurasi |
session | satu run pytest penuh | resource sangat mahal seperti container database |
Perilaku scope terlihat dari fixture berikut:
import pytest
@pytest.fixture(scope="module")
def konfigurasi_toko():
print("membaca konfigurasi toko (satu kali)")
return {"pajak": 0.11}
def test_membaca_pajak(konfigurasi_toko):
assert konfigurasi_toko["pajak"] == 0.11
def test_pajak_tetap_sama(konfigurasi_toko):
assert konfigurasi_toko["pajak"] == 0.11Output yang Dihasilkan (run python3 -m pytest -s -q):
membaca konfigurasi toko (satu kali)
..
2 passed in 0.01sKalimat “membaca konfigurasi toko” hanya tercetak sekali meskipun dua test memakainya — itulah efek scope "module". Flag -s membuat output print() tampil di console.
Fixture dengan yield: Setup dan Teardown #
Bagi resource yang butuh pembersihan (berkas, koneksi, proses), fixture memakai yield sebagai pembatas fase setup dan teardown:
import json
import pytest
from keranjang import KeranjangBelanja
@pytest.fixture
def keranjang_tersimpan(tmp_path):
# bagian setup: berjalan sebelum test
keranjang = KeranjangBelanja()
keranjang.tambah_item("Kopi", 25000, 2)
berkas = tmp_path / "keranjang.json"
berkas.write_text(json.dumps(keranjang.items), encoding="utf-8")
yield keranjang
# bagian teardown: berjalan setelah test selesai
berkas.unlink()
def test_keranjang_tersimpan_di_berkas(keranjang_tersimpan, tmp_path):
berkas = tmp_path / "keranjang.json"
isi = json.loads(berkas.read_text(encoding="utf-8"))
assert isi["Kopi"]["jumlah"] == 2
assert keranjang_tersimpan.total() == 50000Alurnya tiga tahap:
- Sebelum
yield: pytest menyiapkan keranjang, menuliskannya ke berkas JSON, lalu menyerahkan objek ke test. - Test berjalan: menerima
keranjang_tersimpandan memeriksa isi berkas. - Setelah
yield: kode pembersihan (berkas.unlink()) dijalankan otomatis, terlepas test lulus atau gagal.
Fixture Bawaan tmp_path #
pytest membawa puluhan fixture siap pakai; yang paling sering dipakai adalah tmp_path — objek pathlib.Path menuju folder kosong yang unik per test dan dibersihkan otomatis:
def test_menulis_catatan(tmp_path):
berkas = tmp_path / "catatan.txt"
berkas.write_text("Belajar pytest", encoding="utf-8")
assert berkas.read_text(encoding="utf-8") == "Belajar pytest"
assert berkas.exists()Test di atas lolos dalam run yang sama dengan seluruh test fixtures kita:
.... [100%]
4 passed in 0.05sFixture menyelesaikan masalah persiapan data. Masalah berikutnya berbeda wujud: bagaimana menguji puluhan kombinasi masukan tanpa menulis puluhan test — jawabannya parametrize di Chapter Parametrized Tests.