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 #

yaml
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: 14

Anatominya: 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:

bash
python3 verifikasi_yaml.py

Output yang Dihasilkan:

text
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 ada

Simulasi 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:

python
"""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:

  1. Baris 8-16 (TAHAPAN): daftar tahap sebagai data — tambah tahap performance berarti tambah satu baris, bukan menyalin blok baru.
  2. Baris 22-27 (perulangan): subprocess.run menjalankan perintah di direktori proyek; returncode menentukan LULUS/GAGAL, baris terakhir keluarannya diringkas.
  3. Baris 32-36 (vonis akhir): exit code 0 atau 1 — inilah yang dibaca CI sungguhan sebagai status pintu.
bash
python3 ci_lokal.py

Output yang Dihasilkan:

text
== 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 push

Durasi 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.