Perbaikan hari ini tidak boleh melanggar hal yang sudah benar kemarin. Padahal di pipeline data, satu baris yang diubah — pembulatan, urutan sort, definisi kolom — menjalar diam-diam ke seluruh laporan turunannya, dan pemakai laporan yang paling dulu mengetahuinya. Regression testing menutup celah itu dengan cara paling sederhana: simpan output yang sudah disetujui sebagai berkas golden, lalu bandingkan output saat ini terhadapnya pada setiap perubahan kode. Test suite yang tadinya hanya menguji “apakah benar” kini juga menguji “apakah tetap sama”.

Golden File sebagai Kunci Perilaku #

AspekBanding isi (CSV/JSON)Banding hash
Yang terbaca saat merahfield, baris, nilai lama menjadi baruhanya “berbeda”
Tempat di repositoriberkas datar yang mudah ditinjausatu baris teks
Cocok untuktabel ringkas hasil agregatberkas parquet besar

Untuk tabel agregat yang ringkas, banding isi memberi reviewer diff yang bisa dibaca; untuk berkas parquet besar, cukup simpan hash SHA-256 di repositori dan bandingkan hash-nya — hash yang sama membuktikan isi sama tanpa membuka berkasnya.

Objek yang dijaga adalah hitung_ringkasan — agregat penjualan TokoKita per kota, penerapan langsung pola transformasi pada chapter Data Transformation Testing:

python
"""Transformasi ringkasan penjualan per kota — objek yang dijaga regression test."""
import numpy as np
import pandas as pd


def buat_df_pesanan(seed: int = 42, n: int = 500) -> pd.DataFrame:
    """Data uji deterministik: seed tetap menghasilkan baris yang sama."""
    rng = np.random.default_rng(seed)
    return pd.DataFrame({
        "kota": rng.choice(["Jakarta", "Bandung", "Surabaya", "Medan"], n),
        "total": rng.integers(25000, 500000, n),
    })


def hitung_ringkasan(df: pd.DataFrame) -> pd.DataFrame:
    """Agregat per kota: jumlah pesanan, total, dan rata-rata 2 desimal."""
    ringkas = df.groupby("kota", as_index=False).agg(
        jumlah_pesanan=("total", "size"),
        total=("total", "sum"))
    ringkas["rata_rata"] = (ringkas["total"] / ringkas["jumlah_pesanan"]).round(2)
    return ringkas.sort_values("kota").reset_index(drop=True)

Test berikut membaca golden, membangun ulang output dari data ber-seed, lalu membandingkan keduanya kolom per kolom:

python
"""Regression test: output transform dibandingkan berkas golden."""
import os
from pathlib import Path

import pandas as pd

from transform import buat_df_pesanan, hitung_ringkasan

GOLDEN = Path("golden/ringkasan.csv")


def banding_dengan_golden(sekarang: pd.DataFrame, golden: pd.DataFrame) -> list[str]:
    """Banding dua snapshot; kolom float diberi toleransi pembulatan."""
    beda = []
    if list(sekarang.columns) != list(golden.columns):
        return [f"kolom berubah: {list(sekarang.columns)}"]
    if len(sekarang) != len(golden):
        return [f"jumlah baris berubah: {len(golden)} -> {len(sekarang)}"]
    for kolom in sekarang.columns:
        if pd.api.types.is_float_dtype(sekarang[kolom]):
            meleset = (sekarang[kolom] - golden[kolom]).abs() > 0.01
        else:
            meleset = sekarang[kolom] != golden[kolom]
        if meleset.any():
            i = meleset.idxmax()
            beda.append(f"'{kolom}' baris {i}: {golden[kolom][i]} -> {sekarang[kolom][i]}")
    return beda


def test_ringkasan_sesuai_golden():
    sekarang = hitung_ringkasan(buat_df_pesanan())
    if os.environ.get("PERBARUI_GOLDEN") == "1":
        GOLDEN.parent.mkdir(exist_ok=True)
        sekarang.to_csv(GOLDEN, index=False)
        return
    assert GOLDEN.exists(), "jalankan PERBARUI_GOLDEN=1 sekali untuk membuat golden"
    golden = pd.read_csv(GOLDEN)
    beda = banding_dengan_golden(sekarang, golden)
    assert beda == [], "\n".join(beda)

Mari kita telaah baris-baris kode di atas:

  1. Baris 15-21 pada transform.py (hitung_ringkasan): agregat groupby plus rata-rata dua desimal — angka inilah yang dikunci oleh golden.
  2. Baris 12-27 (banding_dengan_golden): kolom float dibandingkan dengan toleransi 0.01. Pergeseran kecil dari orde operasi floating-point lolos; perubahan perilaku nyata — pembulatan, rumus, satuan — terlihat sebagai baris lama menjadi baru.
  3. Baris 32-35 (cabang PERBARUI_GOLDEN): jalur pembaruan golden; test berhenti setelah menulis berkas. Jalur ini hanya boleh dipakai setelah diff ditinjau manusia.
  4. Baris 39 (assert): daftar beda menjadi pesan kegagalan — persis yang terbaca pada output merah di bawah.
bash
PERBARUI_GOLDEN=1 python3 -m pytest test_regression.py -q

Output yang Dihasilkan:

text
.                                                                        [100%]
1 passed in 1.35s

Perintah tersebut menulis golden/ringkasan.csv sekali, dan isinya menjadi kunci:

csv
kota,jumlah_pesanan,total,rata_rata
Bandung,134,36908065,275433.32
Jakarta,122,31456351,257838.94
Medan,125,31692471,253539.77
Surabaya,119,32815523,275760.7
bash
python3 -m pytest test_regression.py -q

Output yang Dihasilkan:

text
.                                                                        [100%]
1 passed in 1.42s

Sekarang datang skenario yang sesungguhnya: seorang pengembang mengubah pembulatan pada transform.py — .round(2) menjadi .round(0) — demi tampilan dashboard yang “lebih rapi”, tanpa memberi tahu tim keuangan. Perintah yang sama dijalankan ulang:

Output yang Dihasilkan:

text
F                                                                        [100%]
=================================== FAILURES ===================================
_________________________ test_ringkasan_sesuai_golden _________________________

    def test_ringkasan_sesuai_golden():
        sekarang = hitung_ringkasan(buat_df_pesanan())
        if os.environ.get("PERBARUI_GOLDEN") == "1":
            GOLDEN.parent.mkdir(exist_ok=True)
            sekarang.to_csv(GOLDEN, index=False)
            return
        assert GOLDEN.exists(), "jalankan PERBARUI_GOLDEN=1 sekali untuk membuat golden"
        golden = pd.read_csv(GOLDEN)
        beda = banding_dengan_golden(sekarang, golden)
>       assert beda == [], "\n".join(beda)
E       AssertionError: 'rata_rata' baris 0: 275433.32 -> 275433.0

Test hanya melaporkan baris pertama yang meleset; durasi 1.35s–1.42s berubah-ubah antar eksekusi. Mengembalikan .round(2) membuat suite hijau kembali dalam hitungan detik.

Ketika Diff itu Disengaja #

Tidak semua beda adalah bug. Rumus diperbaiki, kolom ditambah, definisi rata-rata diubah atas kesepakatan — pada kasus itu golden yang harus ikut. Alurnya tetap tiga langkah: tinjau diff bersama konsumen output, jalankan PERBARUI_GOLDEN=1, lalu commit kode dan golden dalam satu pull request sehingga reviewer melihat keduanya berubah bersama. Sejarah git tetap menyimpan golden lama, jadi siapa pun dapat melihat kapan angka resmi berubah dan alasannya.

Rujukan resmi untuk pendalaman: dokumentasi pandas . Perilaku pipeline kini terkunci dan setiap diff punya jalur persetujuan; satu dimensi masih terbuka — waktu. Chapter Performance Testing mengubah durasi menjadi angka yang bisa diuji dengan ambang dan baseline.