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:

python
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:

python
import pytest
from keranjang import KeranjangBelanja

@pytest.fixture
def keranjang():
    return KeranjangBelanja()

Test menggunakan fixture cukup dengan mendeklarasikan parameter bernama sama:

python
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() == 75000

Mari kita telaah alur yang terjadi:

  1. Parameter keranjang pada test: pytest membaca nama parameter, mencari fixture bernama keranjang, menjalankan fixture tersebut, lalu menyuntikkan nilai baliknya ke test.
  2. File conftest.py: semua fixture di file ini otomatis tersedia untuk test di folder yang sama dan folder turunannya, tanpa perlu import.
  3. Fixture berjalan per test: setiap test menerima instance KeranjangBelanja yang baru, sehingga test pertama tidak mencemari data test kedua — pola ini menggantikan setUp() pada unittest.

Output yang Dihasilkan (dipangkas dari run python3 -m pytest -v):

text
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:

ScopeFixture dibuat ulangCocok untuk
function (bawaan)setiap testobjek berstatus seperti KeranjangBelanja
classsetiap class testdata mahal yang boleh dibagi antar method dalam satu class
modulesetiap file testkoneksi baca-saja, konfigurasi
sessionsatu run pytest penuhresource sangat mahal seperti container database

Perilaku scope terlihat dari fixture berikut:

python
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.11

Output yang Dihasilkan (run python3 -m pytest -s -q):

text
membaca konfigurasi toko (satu kali)
..
2 passed in 0.01s

Kalimat “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:

python
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() == 50000

Alurnya tiga tahap:

  1. Sebelum yield: pytest menyiapkan keranjang, menuliskannya ke berkas JSON, lalu menyerahkan objek ke test.
  2. Test berjalan: menerima keranjang_tersimpan dan memeriksa isi berkas.
  3. 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:

python
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:

text
....                                                                     [100%]
4 passed in 0.05s

Fixture menyelesaikan masalah persiapan data. Masalah berikutnya berbeda wujud: bagaimana menguji puluhan kombinasi masukan tanpa menulis puluhan test — jawabannya parametrize di Chapter Parametrized Tests.