Chapter Pengujian Kode Python pada section Python untuk Data Engineering sudah memperkenalkan fondasi pytest: test function, assert, fixture, dan parametrize. Sekarang kita angkat fondasi tersebut ke wilayah yang paling sering berubah di pipeline TokoKita: kode transformasi field. Fungsi seperti parser harga Rp 1.500.000, mapping status dari API kasir, dan parser tanggal dua format berhadapan dengan data mentah yang datang dalam ribuan variasi. Kesalahan satu karakter pada fungsi kecil ini menyebar tanpa suara ke seluruh angka laporan turunannya, dan unit test adalah perlindungan termurah untuk lapisan ini: setiap test berjalan dalam hitungan milidetik tanpa satu pun I/O.
Apa yang Membedakan Kode Data #
Kode data didominasi fungsi murni: string mentah masuk, nilai bersih keluar, tanpa menyentuh jaringan atau disk. Tiga pola paling sering muncul bersamaan dalam satu modul transformasi. Pertama, parsing nilai dengan format lokal — harga Indonesia memakai titik sebagai pemisah ribuan dan koma sebagai desimal. Kedua, mapping status — API kasir mengirim lunas atau BELUM LUNAS, sementara warehouse menuntut PAID atau UNPAID. Ketiga, normalisasi waktu — tanggal kadang datang sebagai 2026-03-14, kadang 14/03/2026.
Kedua karakter tersebut membuat unit test kode data punya dua kewajiban tambahan dibanding test aplikasi pada umumnya. Rangkaian variasi input bisa dirinci sejak awal, sehingga parametrize menjadi alat utama, bukan pelengkap. Dan batas-batasnya jelas — string kosong, nol, nilai negatif, tanggal tidak sah — sehingga setiap batas wajib punya test sendiri. Bila suite hanya menguji kasus rata-rata, varian nyata dari lapangan akan lolos ke staging table dan baru ketahuan pemakai di laporan, tepat saat biaya perbaikannya paling besar.
Modul yang Diuji: normalisasi.py #
Modul berikut adalah kumpulan tiga fungsi pembersih field yang kita uji sepanjang chapter ini. Ketiganya sengaja murni — tidak ada akses berkas, jaringan, maupun database — karena itu syarat pertama agar sebuah unit bisa diuji terpisah dari lingkungannya.
"""Modul pembersihan field mentah TokoKita sebelum masuk staging table."""
from datetime import date, datetime
def parse_rupiah(teks: str) -> float:
"""Ubah teks harga format Indonesia menjadi float."""
bersih = teks.strip().removeprefix("Rp").strip()
if not bersih:
raise ValueError("teks rupiah kosong")
# Format Indonesia: titik pemisah ribuan, koma desimal.
bersih = bersih.replace(".", "").replace(",", ".")
nilai = float(bersih)
if nilai < 0:
raise ValueError(f"nilai rupiah tidak boleh negatif: {teks!r}")
return nilai
STATUS_BAKU = {
"lunas": "PAID",
"belum lunas": "UNPAID",
"dikirim": "SHIPPED",
"dibatalkan": "CANCELLED",
}
def map_status(status_mentah: str) -> str:
"""Petakan status mentah dari API kasir ke status baku warehouse."""
kunci = status_mentah.strip().lower()
if kunci not in STATUS_BAKU:
raise ValueError(f"status tidak dikenal: {status_mentah!r}")
return STATUS_BAKU[kunci]
def parse_tanggal(teks: str) -> date:
"""Terima 'YYYY-MM-DD' atau 'DD/MM/YYYY', kembalikan objek date."""
for pola in ("%Y-%m-%d", "%d/%m/%Y"):
try:
return datetime.strptime(teks.strip(), pola).date()
except ValueError:
continue
raise ValueError(f"format tanggal tidak dikenal: {teks!r}")Test Suite dengan Pola AAA #
Pola AAA memberi struktur baku pada tubuh test: Arrange menyiapkan input, Act menjalankan fungsi yang diuji, Assert membandingkan hasil dengan nilai yang dihitung manual. Test pertama memakai ketiga tahap itu secara eksplisit; test berikutnya memakai parametrize agar satu fungsi uji menampung seluruh tabel variasi.
"""Unit test untuk modul normalisasi: AAA, parametrize, kasus batas."""
from datetime import date
import pytest
from normalisasi import map_status, parse_rupiah, parse_tanggal
def test_parse_rupiah_format_standar():
# Arrange: siapkan input pada format paling umum dari kasir.
teks = "Rp 1.500.000"
# Act: jalankan fungsi yang diuji.
hasil = parse_rupiah(teks)
# Assert: bandingkan dengan nilai yang dihitung manual.
assert hasil == 1500000.0
@pytest.mark.parametrize(
"teks, diharapkan",
[
("Rp 1.500.000", 1500000.0),
("Rp25000", 25000.0),
("Rp 0", 0.0),
("Rp 99.999,50", 99999.5),
("1.250.000", 1250000.0),
],
)
def test_parse_rupiah_variasi(teks, diharapkan):
assert parse_rupiah(teks) == diharapkan
@pytest.mark.parametrize("teks", ["", "Rp ", "-Rp 500", "Rp -500"])
def test_parse_rupiah_ditolak(teks):
with pytest.raises(ValueError):
parse_rupiah(teks)
@pytest.mark.parametrize(
"mentah, baku",
[
("lunas", "PAID"),
("LUNAS", "PAID"),
(" Belum Lunas ", "UNPAID"),
("Dikirim", "SHIPPED"),
("dibatalkan", "CANCELLED"),
],
)
def test_map_status_variasi(mentah, baku):
assert map_status(mentah) == baku
def test_map_status_tak_dikenal_ditolak():
with pytest.raises(ValueError, match="status tidak dikenal"):
map_status("pending")
@pytest.mark.parametrize(
"teks, diharapkan",
[
("2026-03-14", date(2026, 3, 14)),
("14/03/2026", date(2026, 3, 14)),
("01/02/2026", date(2026, 2, 1)),
],
)
def test_parse_tanggal_variasi(teks, diharapkan):
assert parse_tanggal(teks) == diharapkan
def test_parse_tanggal_tidak_sah_ditolak():
with pytest.raises(ValueError, match="format tanggal"):
parse_tanggal("31/02/2026")Simpan kedua berkas dalam satu direktori, lalu jalankan pytest dengan opsi -v agar setiap test tampil namanya.
python3 -m pytest -v --no-header test_normalisasi.pyOutput yang Dihasilkan:
============================= test session starts ==============================
collecting ... collected 20 items
test_normalisasi.py::test_parse_rupiah_format_standar PASSED [ 5%]
test_normalisasi.py::test_parse_rupiah_variasi[Rp 1.500.000-1500000.0] PASSED [ 10%]
test_normalisasi.py::test_parse_rupiah_variasi[Rp 99.999,50-99999.5] PASSED [ 25%]
test_normalisasi.py::test_parse_rupiah_ditolak[Rp -500] PASSED [ 50%]
test_normalisasi.py::test_map_status_variasi[lunas-PAID] PASSED [ 55%]
test_normalisasi.py::test_map_status_tak_dikenal_ditolak PASSED [ 80%]
test_normalisasi.py::test_parse_tanggal_variasi[01/02/2026-diharapkan2] PASSED [ 95%]
test_normalisasi.py::test_parse_tanggal_tidak_sah_ditolak PASSED [100%]
... 12 baris test parametrize lainnya tidak ditampilkan ...
============================== 20 passed in 0.13s ==============================Dua puluh test selesai dalam sepersekian detik; angka durasi bervariasi antar mesin, sedangkan 20 passed adalah bagian yang stabil. Untuk siklus cepat sehari-hari, opsi -q memangkas keluaran menjadi ringkasan satu baris, lalu -v dipakai ketika kita perlu melihat test mana yang gagal. Output di atas dipangkas jujur: dua belas baris test parametrize tengah tidak ditampilkan agar contoh tetap ringkas. Mari kita telaah baris-baris kode di atas:
- Baris 9-17 (
test_parse_rupiah_format_standar): ketiga tahap AAA dipisahkan komentar agar pembaca langsung melihat input apa yang disiapkan dan klaim apa yang diperiksa. - Baris 20-31 (
parametrizerupiah): lima variasi input menjadi lima test terpisah, termasuk dua kasus batas —Rp 0sebagai nilai terkecil yang sah danRp 99.999,50yang membuktikan koma desimal terurai benar. - Baris 34-37 (
test_parse_rupiah_ditolak): empat input tak sah — kosong, spasi saja, dan dua bentuk negatif — cukup diperiksa denganpytest.raiseskarena klaimnya hanya “ditolak”. - Baris 40-51 (
test_map_status_variasi): mapping diuji dari lima penulisan berbeda menuju empat status baku; huruf kapital dan spasi di tepi tidak boleh mengubah hasil. - Baris 54-56 (
match): penolakanpendingmemakaimatch="status tidak dikenal"sehingga test gagal dengan pesan spesifik bila teks galat berubah tanpa sengaja. - Baris 59-73 (
parse_tanggal): dua format diuji, lalu01/02/2026menegaskan urutan DD/MM (1 Februari) dan31/02/2026membuktikan tanggal tidak sah ditolak.
Ciri Unit Test yang Sehat untuk Kode Data #
Rangkuman pemeriksaan yang layak dijadikan daftar periksa setiap menulis test baru untuk modul transformasi:
| Ciri | Pertanyaan pemeriksa | Contoh pada normalisasi.py |
|---|---|---|
| Murni dan cepat | apakah test lulus tanpa jaringan, disk, atau database? | semua test hanya memanggil fungsi |
| Deterministik | apakah hasil selalu sama setiap dijalankan? | tidak ada waktu saat ini atau angka acak |
| Batas teruji | apakah kosong, nol, dan negatif punya test? | Rp 0 sah, Rp -500 ditolak |
| Mapping tuntas | apakah semua nilai sah plus contoh penolakan teruji? | lima variasi status + pending |
| Pesan galat dibaca | apakah penolakan memakai match? | match="status tidak dikenal" |
Unit test memangkas galat logika sejak fungsi ditulis. Namun pipeline tersusun dari banyak komponen yang dijahit di sambungannya — SQL join, schema tabel, respons API eksternal — dan sambungan itulah yang tidak tersentuh test unit. Chapter Integration Testing menguji setiap sambungan tersebut.