Empat chapter sebelumnya berlatih membaca dan melacak keanehan setelah kejadian; chapter ini menggeser kancingnya menjadi proaktif. Sebuah fungsi pandas yang benar hari ini bisa salah besok karena input baru, perubahan data sumber, atau upgrade versi. Unit test mengunci perilaku fungsi terhadap data kecil yang hasilnya kita hitung manual, sehingga perubahan apa pun yang menggeser hasil langsung terlihat. Chapter ini membangun fondasi pengujian DataFrame: anatomi satu test, pola perbandingan yang tepat untuk struktur, nilai, dan jalur error, serta cara menjalankannya dengan pytest.
Anatomi Satu Unit Test #
Satu unit test adalah fungsi bernama diawali test_ yang mengikuti pola tiga langkah: siapkan input kecil (arrange), panggil fungsi yang diuji (act), dan bandingkan hasil dengan nilai yang sudah dipastikan benar (assert). Inputnya sengaja kecil — dua sampai empat baris — supaya nilai harapannya bisa dihitung tanpa alat bantu. Test seperti ini bukan pengganti eksplorasi data besar; test menjaga logika, sedangkan eksplorasi menggali insight dari data nyata. Fungsi yang kita uji di chapter ini adalah omzet_per_cabang(), sebuah agregasi sederhana dari data transaksi toko.
Empat Sifat yang Layak Diuji #
Untuk satu fungsi transformasi, empat sifat berikut hampir selalu layak diuji: struktur hasil (kolom dan jumlah baris), nilai (angka per baris), kegagalan yang disengaja (agar kita yakin test memang bekerja), dan jalur error (input tidak sah melempar error yang tepat). Script berikut menguji keempatnya, lalu menjalankan semuanya lewat runner kecil bawaan sehingga bisa dieksekusi langsung tanpa alat tambahan.
import pytest
import pandas as pd
def omzet_per_cabang(df: pd.DataFrame) -> pd.DataFrame:
"""Hitung omzet per cabang dari data transaksi (unit yang diuji)."""
return (
df.assign(total=df["jumlah"] * df["harga"])
.groupby("cabang", as_index=False)["total"]
.sum()
)
def data_uji() -> pd.DataFrame:
"""DataFrame kecil dengan hasil yang sudah dihitung manual."""
return pd.DataFrame(
{
"cabang": ["Jakarta", "Bandung", "Jakarta", "Surabaya"],
"jumlah": [2, 1, 3, 4],
"harga": [10_000, 20_000, 5_000, 2_500],
}
)
def test_kolom_dan_baris():
"""Sifat 1 — struktur: kolom dan jumlah baris sesuai."""
hasil = omzet_per_cabang(data_uji())
assert list(hasil.columns) == ["cabang", "total"]
assert len(hasil) == 3 # Jakarta, Bandung, Surabaya
def test_nilai_total_per_cabang():
"""Sifat 2 — nilai: setiap cabang memuat omzet yang dihitung manual."""
hasil = omzet_per_cabang(data_uji())
harapan = pd.DataFrame(
{
"cabang": ["Bandung", "Jakarta", "Surabaya"],
"total": [20_000, 35_000, 10_000],
}
)
assert hasil.equals(harapan)
def test_angka_dari_asumsi_adalah_kegagalan():
"""Sifat 2 (contoh GAGAL) — angka diambil dari asumsi, bukan data uji."""
hasil = omzet_per_cabang(data_uji())
assert hasil["total"].sum() == 999_999, "total 999.999 hanya asumsi"
def test_kolom_hilang_melempar_keyerror():
"""Sifat 3 — jalur error: kolom yang dibutuhkan tidak ada."""
tanpa_harga = pd.DataFrame({"cabang": ["Jakarta"], "jumlah": [1]})
with pytest.raises(KeyError):
omzet_per_cabang(tanpa_harga)
def jalankan_semua() -> None:
"""Runner kecil: jalankan setiap test_ dan laporkan hasilnya."""
daftar = (
test_kolom_dan_baris,
test_nilai_total_per_cabang,
test_angka_dari_asumsi_adalah_kegagalan,
test_kolom_hilang_melempar_keyerror,
)
for fungsi in daftar:
try:
fungsi()
except Exception as exc:
detail = str(exc).splitlines()[0] if str(exc) else "(tanpa pesan)"
print(f"GAGAL {fungsi.__name__}: {type(exc).__name__}: {detail}")
else:
print(f"OK {fungsi.__name__}")
if __name__ == "__main__":
jalankan_semua()Output yang Dihasilkan:
OK test_kolom_dan_baris
OK test_nilai_total_per_cabang
GAGAL test_angka_dari_asumsi_adalah_kegagalan: AssertionError: total 999.999 hanya asumsi
OK test_kolom_hilang_melempar_keyerrorMari kita bedah bagian pentingnya:
data_uji(): DataFrame empat baris dengan dua transaksi Jakarta, satu Bandung, satu Surabaya. Dua baris untuk satu cabang sengaja disertakan — grup dengan lebih dari satu baris adalah kasus yang paling sering salah dihitung.test_kolom_dan_baris(): memeriksa struktur denganassert list(hasil.columns) == [...]danlen(hasil) == 3. Perbandingan list kolom akan menangkap perubahan nama atau urutan kolom yang sering luput dari perhitungan.test_nilai_total_per_cabang(): membandingkan seluruh hasil dengan DataFrame harapan yang angkanya dihitung manual: Bandung 20.000, Jakarta 35.000, Surabaya 10.000. MetodeDataFrame.equals()membandingkan bentuk, dtype, dan nilai sekaligus, termasuk nilaiNaNyang dianggap sama posisi.test_angka_dari_asumsi_adalah_kegagalan(): test yang sengaja gagal. Total sebenarnya 65.000, jadiassertmelemparAssertionErrordengan pesantotal 999.999 hanya asumsi. Melihat tampilan kegagalan sekali-sekali berguna: runner mencetakGAGALdiikuti pesan, dan test yang gagal tidak menghentikan test lain.test_kolom_hilang_melempar_keyerror(): memanggilomzet_per_cabang()dengan DataFrame tanpa kolomhargadi dalam blokwith pytest.raises(KeyError). Konteks manager ini hanya lolos bila error yang tepat terlempar; bila tidak ada error atau error salah kelas, blok tersebut menggagalkan test.jalankan_semua(): runner kecil yang memanggil setiap test dalamtry/exceptdan mencetak statusnya. Perhatikan polastr(exc).splitlines()[0] if str(exc) else "(tanpa pesan)"— pesanAssertionErrorpolos tanpa argumen bisa kosong, jadi runner perlu penangan tersebut.
Menjalankan Test yang Sama dengan pytest #
Runner kecil cocok untuk demonstrasi, tetapi pytest memberi banyak fasilitas: koleksi otomatis file test_*.py, laporan kegagalan yang detail, fixture untuk penyiapan data, dan parameterisasi. Versi pytest dari test yang sama cukup disimpan sebagai test_transaksi.py dan dijalankan dengan python3 -m pytest test_transaksi.py -q; pytest mencetak titik per test dan ringkasan seperti 3 passed.
import pandas as pd
import pytest
from pandas.testing import assert_frame_equal
from uji_omzet import data_uji, omzet_per_cabang
def test_struktur():
hasil = omzet_per_cabang(data_uji())
assert list(hasil.columns) == ["cabang", "total"]
assert len(hasil) == 3
def test_nilai_dengan_assert_frame_equal():
hasil = omzet_per_cabang(data_uji())
harapan = pd.DataFrame(
{
"cabang": ["Bandung", "Jakarta", "Surabaya"],
"total": [20_000, 35_000, 10_000],
}
)
assert_frame_equal(hasil, harapan)
def test_kolom_hilang():
tanpa_harga = pd.DataFrame({"cabang": ["Jakarta"], "jumlah": [1]})
with pytest.raises(KeyError):
omzet_per_cabang(tanpa_harga)Dua tambahan pytest yang layak dicatat: @pytest.mark.parametrize menjalankan satu fungsi test berkali-kali dengan pasangan input-harapan berbeda, dan fixture seperti tmp_path menyediakan folder sementara untuk test yang membaca atau menulis file.
Dengan keempat sifat dasar terkunci, langkah berikutnya adalah mengetahui opsi apa saja yang bisa diatur saat dua DataFrame dibandingkan — termasuk toleransi pembulatan float dan pemeriksaan dtype — yang dibahas penuh pada chapter berikutnya.
Referensi resmi untuk chapter ini: dokumentasi pytest , pytest.raises pada referensi API pytest , dan pandas.DataFrame.equals .