Test hanya sebaik data yang dipakainya. Pipeline TokoKita memroses pesanan dengan null, nama kota tak baku, dan duplikat — data uji yang selalu bersih akan meloloskan kode yang gagal di produksi, sementara menyalin data produksi mentah ke test membocorkan data pelanggan dan membuat test lambat. Di antara keduanya ada fixture: data kecil yang disiapkan otomatis per test, deterministik antar run, dan sengaja memuat kasus tepi yang paling sering menjatuhkan pipeline. Chapter ini menyusun fixture untuk tiga kebutuhan khas data engineering — DataFrame, basis data sementara, dan baris tunggal — lalu merapikannya menjadi katalog kasus tepi.
Tiga Sumber Data Uji #
| Aspek | Sintetis (factory/seed) | Produksi tersamar | Snapshot fixture |
|---|---|---|---|
| Asal | dibangkitkan kode dengan seed | cuplikan produksi, PII disamarkan | hasil run pipeline yang disimpan |
| Determinisme | penuh, seed tetap | mengikuti cuplikan | beku sejak disimpan |
| Biaya & risiko | murah, tanpa risiko bocor | mahal, perlu aturan penyamaran | murah, bisa membengkak |
| Cocok untuk | unit, property-based test | integration menjelang produksi | regression, golden file |
Sintetis adalah pilihan utama untuk siklus harian; snapshot dipakai sebagai kunci perilaku pada chapter Regression Testing; dan penyamaran produksi mengikuti aturan pada section Keamanan dan Tata Kelola Data sebelum data keluar lingkungan produksi.
Fixture, Factory, dan Kasus Tepi dalam Satu Berkas #
Berkas berikut memuat tiga pola sekaligus: fixture DataFrame ber-seed, fixture SQLite di direktori sementara, dan factory baris uji yang ditimpa seperlunya. Contoh kasus tepi — unicode, kota tak baku, duplikat kunci, tanggal batas — ikut di dalamnya.
"""Fixture untuk kode data: DataFrame ber-seed, SQLite sementara, factory baris."""
import datetime as dt
import sqlite3
import numpy as np
import pandas as pd
import pytest
KOTA = ["Jakarta", "Bandung", "Surabaya", "Medan"]
def buat_pesanan(order_id=1, pelanggan="Budi", kota="Jakarta", total=100000,
status="dibayar", **ubahan):
"""Factory satu baris pesanan: nilai bawaan, ditimpa seperlunya per test."""
baris = {"order_id": order_id, "pelanggan": pelanggan, "kota": kota,
"total": total, "status": status}
baris.update(ubahan)
return baris
def validasi_baris(baris):
"""Aturan bisnis minimum yang dipegang sisi konsumen."""
masalah = []
if not baris["pelanggan"]:
masalah.append("pelanggan kosong")
if baris["total"] < 0:
masalah.append("total negatif")
if baris["kota"] not in KOTA:
masalah.append("kota tak dikenal")
return masalah
@pytest.fixture
def df_pesanan():
"""DataFrame sintetis 200 baris; seed 42 membuat hasil identik antar run."""
rng = np.random.default_rng(42)
n = 200
return pd.DataFrame({
"order_id": np.arange(1, n + 1),
"kota": rng.choice(KOTA, n),
"total": rng.integers(25000, 500000, n),
"status": rng.choice(["dibayar", "kirim"], n, p=[0.7, 0.3]),
})
@pytest.fixture
def db_tokokita(tmp_path):
"""SQLite per-test di tmp_path; dibersihkan otomatis setelah selesai."""
koneksi = sqlite3.connect(tmp_path / "tokokita.db")
koneksi.execute("CREATE TABLE pesanan ("
"order_id INTEGER PRIMARY KEY, pelanggan TEXT NOT NULL,"
" kota TEXT, total INTEGER, status TEXT)")
awal = [buat_pesanan(1), buat_pesanan(2, "Siti", "Bandung", 250000),
buat_pesanan(3, "Dewi", "Medan", 175000, "kirim")]
koneksi.executemany(
"INSERT INTO pesanan VALUES (:order_id, :pelanggan, :kota,"
" :total, :status)", awal)
koneksi.commit()
yield koneksi
koneksi.close()
def test_df_ber_seed_deterministik(df_pesanan):
assert len(df_pesanan) == 200
assert round(df_pesanan["total"].mean(), 2) == 255110.42
assert set(df_pesanan["kota"]).issubset(KOTA)
def test_factory_menimpa_sebagian():
baris = buat_pesanan(order_id=9, total=0)
assert (baris["order_id"], baris["total"]) == (9, 0)
assert baris["pelanggan"] == "Budi"
def test_sqlite_fixture(db_tokokita):
jumlah, total = db_tokokita.execute(
"SELECT COUNT(*), SUM(total) FROM pesanan").fetchone()
assert (jumlah, total) == (3, 525000)
def test_duplikat_order_id_ditolak(db_tokokita):
with pytest.raises(sqlite3.IntegrityError):
db_tokokita.execute(
"INSERT INTO pesanan VALUES (:order_id, :pelanggan, :kota,"
" :total, :status)", buat_pesanan(1, "Rina", "Bandung"))
def test_tanggal_batas_kabisat():
def valid(t):
try:
return dt.date.fromisoformat(t) is not None
except ValueError:
return False
assert valid("2024-02-29")
assert not valid("2023-02-29")
@pytest.mark.parametrize("kota, diharapkan", [
("Surabáya", "kota tak dikenal"),
("Ujung Pandang", "kota tak dikenal"),
])
def test_validasi_kasus_tepi(kota, diharapkan):
masalah = validasi_baris(buat_pesanan(10, kota=kota))
assert diharapkan in masalahMari kita telaah baris-baris kode di atas:
- Baris 12-18 (
buat_pesanan): factory dengan nilai bawaan. Argumen**ubahanmemungkinkan test menimpa satu-dua field tanpa menulis ulang seluruh baris — pola paling hemat untuk data uji. - Baris 33-43 (
df_pesanan): fixture DataFrame.np.random.default_rng(42)menghasilkan 200 baris yang identik di setiap run, sehingga mean255110.42bisa diklaim lewat assert tanpa takut goyah. - Baris 46-60 (
db_tokokita): fixture SQLite memakaitmp_pathbawaan pytest — direktori sementara unik per test.yieldmenyerahkan koneksi ke test; baris setelahnya berjalan saat pembersihan, apa pun hasil testnya. - Baris 98-104 (
parametrizekasus tepi): satu fungsi uji dijalankan sekali per baris data;Surabáyadengan diakritik terbukti dianggap kota lain olehvalidasi_baris.
python3 -m pytest test_fixtures_data.py -qOutput yang Dihasilkan:
....... [100%]
7 passed in 1.38sDeretan tujuh titik adalah tujuh test: satu DataFrame ber-seed, satu factory, satu agregat SQLite, satu penolakan duplikat, satu tanggal kabisat, dan dua varian kasus tepi. Durasi 1.38s berubah-ubah antar eksekusi; jumlah passed yang stabil.
Determinisme dan Katalog Kasus Tepi #
Determinisme lahir dari seed yang tetap dan waktu yang tidak ikut diuji. Aturan praktisnya: setiap sumber acak di fixture wajib punya seed eksplisit, dan setiap jam/ tanggal diuji sebagai nilai tetap — bukan datetime.now() yang berbeda tiap run. Tanpa dua aturan itu, test berkedip merah-hijau tanpa perubahan kode, dan kepercayaan pada suite runtuh.
| Kasus tepi | Contoh | Alasan diuji |
|---|---|---|
| Nilai kosong | pelanggan=None, total=0 | pipeline nyata memang menerima null |
| Unicode | Surabáya vs Surabaya | perbandingan string peka diakritik |
| Duplikat kunci | order_id sama dua kali | integritas kunci, idempotensi |
| Tanggal batas | 29 Feb ada di 2024, tidak di 2023 | parsing dan validasi rentang |
| Nilai ekstrem | total di batas minimum kontrak | galat paling sering di tepi rentang |
Rujukan resmi untuk pendalaman: dokumentasi pytest
— terutama bab fixture dan tmp_path. Data uji kini murah dan stabil; berikutnya kita mengunci perilaku pipeline agar perubahan apa pun terlihat — pokok bahasan chapter Regression Testing.