Test yang paling bagus sekalipun tidak berguna bila hanya dijalankan saat seseorang ingat. Di tim TokoKita, disiplin individu selalu kalah dengan tugas yang menumpuk: test dilewati, commit langsung ke main, dan kegagalan baru terlihat seminggu kemudian. Continuous integration (CI) memindahkan disiplin itu ke mesin: setiap push dan pull request memicu rangkaian gerbang — lint, unit, integration, contract, data diff — dan merge hanya terjadi setelah semuanya hijau. Chapter ini menutup section dengan menyusun pintu tersebut: workflow GitHub Actions untuk pipeline data, lalu simulasi yang sama di mesin lokal.
Workflow dalam GitHub Actions #
name: CI Pipeline Data
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:18
env:
POSTGRES_PASSWORD: YOUR_DATABASE_PASSWORD
POSTGRES_DB: tokokita_test
ports: ["5432:5432"]
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 5s
--health-timeout 5s
--health-retries 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- run: pip install -r requirements.txt
- name: Lint
run: ruff check .
- name: Unit test
run: python3 -m pytest tests/unit -q
- name: Integration test
run: python3 -m pytest tests/integration -q
env:
DATABASE_URL: postgres://postgres:YOUR_DATABASE_PASSWORD@localhost:5432/tokokita_test
- name: Contract test
run: python3 -m pytest tests/contract -q
- name: Data diff terhadap golden
run: python3 -m pytest tests/regression -q
- uses: actions/upload-artifact@v4
if: always()
with:
name: laporan-uji
path: reports/
retention-days: 14Anatominya: on mendefinisikan pemicu (push ke main dan semua pull request); jobs.test.services menyalakan container PostgreSQL 18 dengan pemeriksaan kesehatan pg_isready, sehingga integration test memakai basis data sungguhan yang dibuang setelah selesai — pola service container
di GitHub Actions
. cache: pip menyimpan dependensi antar run; upload-artifact menyimpan laporan selama 14 hari dengan if: always() supaya bukti tetap terunggah walau test merah.
Sebelum dipakai, workflow kita validasi dengan yaml.safe_load — cara yang sama dengan chapter Data Contracts membaca kontraknya:
python3 verifikasi_yaml.pyOutput yang Dihasilkan:
pemicu : ['push', 'pull_request']
runner : ubuntu-latest
layanan: ['postgres']
langkah: actions/checkout@v4
langkah: actions/setup-python@v5
langkah: pip
langkah: Lint
langkah: Unit test
langkah: Integration test
langkah: Contract test
langkah: Data diff terhadap golden
langkah: actions/upload-artifact@v4
struktur workflow valid: service postgres + retensi artifact adaSimulasi CI di Mesin Lokal #
Menunggu runner ada kuotanya membosankan; untungnya urutan gerbang bisa disimulasikan di mesin lokal. Proyek mininya berisi src/transform.py (normalisasi kota dan agregat), tests/ untuk unit dan integration ala chapter Unit Testing dan Integration Testing, kontrak.yaml beserta pemeriksanya dari chapter Contract Testing, dan berkas golden dari chapter Regression Testing. Skrip berikut menjalankan lima tahap secara berurutan:
"""Simulasi CI lokal: tahapan berurutan, ringkasan lulus/gagal di akhir."""
import subprocess
import sys
from pathlib import Path
PROYEK = Path(__file__).parent / "proyek"
TAHAPAN = [
("lint", ["python3", "-m", "py_compile", "src/transform.py",
"tests/test_unit.py", "tests/test_integrasi.py",
"tests/test_regression.py", "cek_kontrak.py"]),
("unit", ["python3", "-m", "pytest", "tests/test_unit.py", "-q"]),
("integrasi", ["python3", "-m", "pytest", "tests/test_integrasi.py", "-q"]),
("kontrak", ["python3", "cek_kontrak.py"]),
("data-diff", ["python3", "-m", "pytest", "tests/test_regression.py", "-q"]),
]
def main() -> int:
print("== CI lokal: 5 tahap ==")
ringkas = []
for nama, perintah in TAHAPAN:
hasil = subprocess.run(perintah, cwd=PROYEK, capture_output=True, text=True)
baris_akhir = (hasil.stdout.strip().splitlines()[-1]
if hasil.stdout.strip() else "tanpa keluaran")
ringkas.append((nama, hasil.returncode == 0, baris_akhir))
print(f" [{nama:9}] {'LULUS' if hasil.returncode == 0 else 'GAGAL'}")
print("\n== Ringkasan ==")
for nama, lulus, baris_akhir in ringkas:
tanda = "LULUS" if lulus else "GAGAL"
print(f" {nama:10} {tanda} ({baris_akhir})")
if all(lulus for _, lulus, _ in ringkas):
print("CI LOKAL: LULUS — aman untuk push")
return 0
print("CI LOKAL: GAGAL — perbaiki tahap merah di atas")
return 1
if __name__ == "__main__":
sys.exit(main())Mari kita telaah baris-baris kode di atas:
- Baris 8-16 (
TAHAPAN): daftar tahap sebagai data — tambah tahap performance berarti tambah satu baris, bukan menyalin blok baru. - Baris 22-27 (perulangan):
subprocess.runmenjalankan perintah di direktori proyek;returncodemenentukan LULUS/GAGAL, baris terakhir keluarannya diringkas. - Baris 32-36 (vonis akhir): exit code
0atau1— inilah yang dibaca CI sungguhan sebagai status pintu.
python3 ci_lokal.pyOutput yang Dihasilkan:
== CI lokal: 5 tahap ==
[lint ] LULUS
[unit ] LULUS
[integrasi] LULUS
[kontrak ] LULUS
[data-diff] LULUS
== Ringkasan ==
lint LULUS (tanpa keluaran)
unit LULUS (2 passed in 1.38s)
integrasi LULUS (1 passed in 1.33s)
kontrak LULUS (kontrak ringkasan_kota v1 LULUS: 3 baris, 4 kolom)
data-diff LULUS (1 passed in 1.32s)
CI LOKAL: LULUS — aman untuk pushDurasi per tahap (1.38s, 1.33s, 1.32s) berubah-ubah antar eksekusi; vonis LULUS/GAGAL yang stabil. Bentuk pintu lengkapnya di GitHub:
Menjaga Pintunya #
Pintu baru berguna bila tidak bisa dilewati. Branch protection di GitHub mewajibkan status check hijau sebelum merge, melarang push langsung ke main, dan menahan pull request tanpa tinjauan — kegagalan yang dulu bocor ke produksi kini berhenti di gerbang. Untuk suite yang membesar, bagi test ke beberapa proses (pytest-xdist) dan pisahkan test lambat ke job tersendiri agar umpan balik tetap cepat; test yang lebih dari sepuluh menit pada akhirnya dihindari orang.
Dengan lima gerbang ini, section Data Engineering Testing ditutup: perilaku dikunci regression, janji antar tim dijaga contract, anggaran waktu diuji performance, dan semuanya dirangkai CI. Pintu berikutnya adalah membawa pipeline yang sudah teruji ke lingkungan produksi dan menjaganya tetap sehat di sana — wilayah Section Deployment dan Operasional.