Kode pipeline yang tidak teruji adalah kotak hitam: tidak ada yang tahu apakah angka laporan benar sampai seseorang menanyakan asal-usulnya. Kesalahan kecil — tanda kurang, pembulatan, kolom tertukar — menular diam-diam ke seluruh laporan turunannya. Pengujian otomatis menutup celah itu: setiap perubahan kode dijalankan ulang terhadap rangkaian uji, dan kegagalan muncul dalam hitungan detik, bukan berminggu-minggu kemudian di hadapan pemakai laporan. Kita pakai pytest, kerangka pengujian standar de facto ekosistem Python.
Modul yang Diuji #
Objek uji kita adalah transform.py, modul kecil bertugas membersihkan satu record transaksi mentah menjadi record siap muat. Modul ini sengaja dibuat mungil agar fokus kita tetap pada cara mengujinya.
def normalisasi_kota(kota: str) -> str:
kota = kota.strip().upper()
if not kota:
raise ValueError("nama kota tidak boleh kosong")
return kota
def hitung_pajak(total: float, tarif: float = 0.11) -> float:
if total < 0:
raise ValueError("total transaksi tidak boleh negatif")
return round(total * tarif, 2)
def transform_transaksi(record: dict) -> dict:
kota = normalisasi_kota(record["kota"])
total = float(record["total"])
if total <= 0:
raise ValueError(f"total harus positif, ditemukan: {total}")
return {
"kota": kota,
"total": total,
"pajak": hitung_pajak(total),
}Sebelum menulis test suite, kita pastikan modulnya berjalan dengan pemeriksaan manual sederhana.
from transform import normalisasi_kota, transform_transaksi
print(normalisasi_kota(" surabaya "))
print(transform_transaksi({"kota": "medan", "total": "87500"}))Output yang Dihasilkan:
SURABAYA
{'kota': 'MEDAN', 'total': 87500.0, 'pajak': 9625.0}Pemeriksaan manual membuktikan fungsi bekerja pada satu contoh. Tugas test suite adalah membuktikannya pada banyak contoh sekaligus — termasuk kasus yang harus menolak input.
Menulis Test: assert, fixture, dan parametrize #
Test suite berikut meletakkan tiga konsep pytest dalam satu file. Fungsi uji diawali test_; klaim dinyatakan dengan assert; pytest.raises memeriksa exception; fixture menyediakan data bersama; parametrize menguji satu fungsi terhadap banyak kombinasi masukan.
import pytest
from transform import hitung_pajak, normalisasi_kota, transform_transaksi
@pytest.fixture
def transaksi_contoh():
return {"kota": " jakarta ", "total": "150000"}
def test_normalisasi_kota():
assert normalisasi_kota(" bandung ") == "BANDUNG"
@pytest.mark.parametrize(
"total, tarif, diharapkan",
[
(100000, 0.11, 11000.0),
(275000, 0.11, 30250.0),
(100000, 0.0, 0.0),
],
)
def test_hitung_pajak(total, tarif, diharapkan):
assert hitung_pajak(total, tarif) == diharapkan
def test_hitung_pajak_negatif_ditolak():
with pytest.raises(ValueError):
hitung_pajak(-500)
def test_normalisasi_kota_kosong_ditolak():
with pytest.raises(ValueError):
normalisasi_kota(" ")
def test_transform_transaksi(transaksi_contoh):
hasil = transform_transaksi(transaksi_contoh)
assert hasil["kota"] == "JAKARTA"
assert hasil["total"] == 150000.0
assert hasil["pajak"] == 16500.0
def test_transform_transaksi_total_nol_ditolak():
with pytest.raises(ValueError):
transform_transaksi({"kota": "Medan", "total": "0"})Mari kita telaah baris-baris kode di atas:
- Baris 6-8 (
@pytest.fixture): fixturetransaksi_contohmenyiapkan data uji. Setiap test yang meminta parameter bernamatransaksi_contohmenerima salinan data ini — fixture memisahkan penyiapan data dari klaim pengujian. - Baris 11-12 (
assert): bentuk paling dasar. Bila hasilnormalisasi_kotatidak sama dengan"BANDUNG", assert mengangkat kegagalan dan pytest menampilkan nilai aktual di samping nilai yang diharapkan. - Baris 15-24 (
@pytest.mark.parametrize): satu fungsi uji dijalankan tiga kali — sekali per baris data. Setiap kombinasi menjadi test tersendiri dengan identitas unik, misalnyatest_hitung_pajak[275000-0.11-30250.0]. - Baris 27-29 (
pytest.raises): blokwithini lulus justru karenaValueErrorterangkat. Tanpa exception, test justru gagal — pola standar untuk menguji validasi pipeline. - Baris 37-41 (
test_transform_transaksi): test ini memakai fixture melalui nama parameternya. Tigaassertmemeriksa hasil transformasi secara terpisah: kota ternormalisasi, total jadi float, pajak terhitung.
Menjalankan Test Suite #
pytest mengumpulkan semua file berpola test_*.py, menjalankan fungsi berpola test_*, lalu meringkas hasilnya. Perintah berikut dijalankan dari direktori yang sama dengan transform.py dan test_transform.py.
python3 -m pytest -qOutput yang Dihasilkan:
........ [100%]
8 passed in 0.12sDeretan delapan titik mewakili delapan test yang lulus: satu test_normalisasi_kota, tiga varian test_hitung_pajak dari parametrize, dua uji penolakan exception, dan dua uji transform_transaksi. Opsi -q memangkas keluaran menjadi ringkasan satu baris; tanpa opsi itu, pytest menampilkan nama setiap test dan durasinya. Angka 0.12s berubah-ubah antar eksekusi mengikuti beban mesin — jumlah 8 passed adalah bagian yang stabil.
Struktur Test untuk Proyek Pipeline #
Untuk proyek yang lebih besar, test dipisahkan ke direktori khusus dengan struktur yang mengikuti kode produksi.
proyek-pipeline/
├── src/
│ └── transform.py
├── tests/
│ ├── conftest.py
│ ├── test_transform.py
│ └── fixtures/
│ └── transaksi_contoh.json
└── pyproject.tomlFile conftest.py menampung fixture yang dipakai lintas file test — pytest memuatnya secara otomatis tanpa perlu impor. Direktori fixtures/ menyimpan berkas data uji, misalnya cuplikan CSV produksi yang sudah disamarkan. Untuk test yang menulis berkas sementara, fixture bawaan tmp_path menyediakan direktori sementara per test dan dibersihkan otomatis setelah selesai.
Rujukan resmi untuk pendalaman: dokumentasi pytest
dan modul unittest
sebagai pembanding bawaan. Dengan fondasi Python lengkap — dari struktur data hingga pengujian — kita siap berpindah ke wilayah data itu sendiri: Section Dasar SQL membahas bahasa yang menjadi lingua franca seluruh pipeline data.