Unit Test as Guard #

Ada dua cara berbeda dalam memandang unit test. Cara pertama: test adalah kewajiban yang harus dipenuhi sebelum PR bisa di-merge — angka coverage harus mencapai threshold, CI harus hijau, selesai. Cara ini menghasilkan test yang ada tapi tidak bermakna: assertion yang selalu lulus tanpa memverifikasi behavior apapun, test yang ditulis setelah kode selesai hanya untuk mengejar angka, coverage 80% yang menyembunyikan path kritis yang tidak tercover sama sekali.

Cara kedua: test adalah bukti bahwa kode bekerja sesuai ekspektasi — di semua kasus yang penting, termasuk yang tidak nyaman. Test adalah dokumentasi executable yang menjelaskan kepada engineer berikutnya apa yang seharusnya terjadi dan apa yang tidak. Dan test sebagai guard di CI adalah mekanisme otomatis yang memastikan tidak ada perubahan yang bisa masuk ke codebase tanpa membuktikan dirinya.

Perbedaan antara keduanya bukan pada tooling atau threshold angka — tapi pada pemahaman tentang apa sebenarnya fungsi test dalam siklus development. Artikel ini membahas unit test dari sudut pandang guard di pull request: mengapa ia perlu ada, bagaimana ia seharusnya bekerja, dan apa yang membuatnya benar-benar efektif sebagai gatekeeper kualitas.

Mengapa Test Perlu Menjadi Guard, Bukan Opsional #

Reviewer manusia memiliki kemampuan luar biasa untuk mendeteksi masalah desain, inkonsistensi arsitektur, dan potensi edge case — tapi ia memiliki satu keterbatasan fundamental: ia tidak bisa mengeksekusi kode di kepalanya untuk semua skenario yang mungkin terjadi. Seorang reviewer bisa melihat bahwa ada case yang tidak ditangani, tapi tidak bisa membuktikan bahwa case tersebut benar-benar menghasilkan output yang salah.

Test bisa melakukan apa yang reviewer tidak bisa: mengeksekusi setiap skenario secara deterministik dan melaporkan hasilnya dengan pasti. Reviewer dan test bukan substitusi satu sama lain — mereka saling melengkapi.

flowchart LR
    A[PR dibuka] --> B[CI Pipeline]
    B --> C{Unit Test}
    C -->|Gagal| D[PR diblokir\nautomatis]
    D --> E[Author perbaiki\ntanpa perlu\nmenunggu reviewer]
    E --> B
    C -->|Lulus| F[Code Review]
    F --> G{Reviewer}
    G -->|Request Changes| H[Author revisi]
    H --> B
    G -->|Approve| I[Merge ke main]

    style D fill:#ffcccc
    style I fill:#ccffcc

Tanpa guard ini, dua skenario buruk bisa terjadi. Pertama: reviewer menemukan bug yang seharusnya bisa dideteksi test — waktu reviewer terbuang untuk hal yang bisa diotomasi. Kedua: bug tidak terdeteksi sama sekali karena reviewer tidak memeriksa setiap path kode — dan bug masuk ke main.


Shift-Left: Semakin Awal Ditemukan, Semakin Murah #

Prinsip shift-left testing adalah memindahkan pengujian ke titik paling awal dalam siklus development — karena biaya menemukan dan memperbaiki bug tumbuh secara eksponensial seiring waktu.

Biaya menemukan bug di berbagai tahap:

  Saat menulis kode        → paling murah
  (developer sadar sendiri)   biaya: beberapa menit refactor

  Saat unit test gagal     → masih murah
  (CI guard menangkap)        biaya: puluhan menit debug dan fix

  Saat code review         → mulai mahal
  (reviewer menemukan)        biaya: context switch, revisi, re-review

  Saat QA testing          → mahal
  (ditemukan di staging)      biaya: bug report, investigate, fix, re-deploy

  Saat production incident → sangat mahal
  (user merasakannya)         biaya: incident response, hotfix, komunikasi,
                              reputasi, potential data loss

Unit test sebagai CI guard adalah implementasi konkret dari shift-left: setiap push ke branch PR otomatis menjalankan seluruh suite test, dan jika ada yang gagal, tidak ada yang bisa merge sampai diperbaiki. Bug yang seharusnya menjadi incident produksi menjadi feedback loop selama beberapa menit di local development.


Apa yang Membuat Test Benar-benar Berguna sebagai Guard #

Ini adalah bagian yang paling sering diabaikan dalam diskusi “wajib ada unit test”. Test yang ada tapi tidak bisa gagal ketika behavior berubah adalah test yang tidak melindungi apapun — ia hanya memberikan false confidence.

Test yang Bisa Gagal #

Test yang baik adalah test yang akan gagal jika logika yang diuji berubah menjadi salah. Ini terdengar trivial tapi banyak test yang ditulis tidak memenuhi kriteria ini.

# ANTI-PATTERN: test yang tidak bisa gagal (false positive)
def test_calculate_discount():
    result = calculate_discount(100, "PROMO10")
    assert result is not None   # ← selalu true, tidak memverifikasi apapun

# ANTI-PATTERN: test yang tidak memanggil assertion
def test_process_payment():
    process_payment(order_id=42, amount=150000)
    # tidak ada assertion — test "lulus" apapun yang terjadi

# BENAR: test yang benar-benar memverifikasi behavior
def test_calculate_discount_with_valid_promo():
    # Arrange
    price = 100_000
    promo_code = "PROMO10"

    # Act
    discounted_price = calculate_discount(price, promo_code)

    # Assert
    assert discounted_price == 90_000  # 10% discount dari 100.000

def test_calculate_discount_with_invalid_promo():
    # Jika promo tidak valid, harga tidak boleh berubah
    price = 100_000
    discounted_price = calculate_discount(price, "INVALID_CODE")
    assert discounted_price == 100_000

def test_calculate_discount_with_zero_price():
    # Edge case: harga 0 harus tetap 0 setelah discount
    assert calculate_discount(0, "PROMO10") == 0

Test yang Menguji Behavior, Bukan Implementasi #

Test yang terlalu terikat pada detail implementasi akan rusak setiap kali refactor dilakukan — bahkan ketika behavior tidak berubah. Test yang baik menguji apa yang dilakukan kode, bukan bagaimana ia melakukannya.

# ANTI-PATTERN: test yang terlalu terikat ke implementasi
def test_process_order():
    mock_db = Mock()
    mock_email = Mock()
    service = OrderService(db=mock_db, email=mock_email)

    service.process_order(order_id=42)

    # Mengecek detail implementasi internal, bukan behavior
    mock_db.session.begin.assert_called_once()  # ← implementasi detail
    mock_db.query.assert_called_with("SELECT...")  # ← implementasi detail
    mock_email.send_template.assert_called_with("order_confirmation", ...)

    # Jika implementasi DB berubah (tetapi behavior sama),
    # test ini akan gagal — padahal tidak ada yang salah

# BENAR: test yang fokus pada behavior yang bisa diobservasi
def test_process_order_sends_confirmation():
    # Setup: order yang valid
    order = create_test_order(id=42, user_id=1, amount=150000)
    fake_email_service = FakeEmailService()  # test double, bukan mock ketat
    service = OrderService(email=fake_email_service)

    service.process_order(order)

    # Assert behavior yang bisa diobservasi dari luar
    assert fake_email_service.was_confirmation_sent_to(order.user_email)
    assert order.status == "processing"
    # Tidak peduli bagaimana internal DB bekerja, hanya peduli hasilnya

Test yang Mencakup Happy Path DAN Edge Case #

Test hanya untuk happy path memberikan confidence yang palsu. Sebagian besar bug ada di edge case — input yang kosong, nilai yang di luar batas, kondisi race, koneksi yang terputus.

# Suite test yang komprehensif untuk fungsi transfer saldo
class TestTransferBalance:

    # Happy path
    def test_transfer_success_reduces_sender_balance(self):
        sender = create_wallet(balance=100_000)
        receiver = create_wallet(balance=50_000)
        transfer(sender, receiver, amount=30_000)
        assert sender.balance == 70_000

    def test_transfer_success_increases_receiver_balance(self):
        sender = create_wallet(balance=100_000)
        receiver = create_wallet(balance=50_000)
        transfer(sender, receiver, amount=30_000)
        assert receiver.balance == 80_000

    # Edge case: batas saldo
    def test_transfer_fails_when_balance_insufficient(self):
        sender = create_wallet(balance=10_000)
        receiver = create_wallet(balance=0)
        with pytest.raises(InsufficientBalanceError):
            transfer(sender, receiver, amount=50_000)

    def test_transfer_exact_balance_succeeds(self):
        # Transfer tepat sebesar saldo yang ada
        sender = create_wallet(balance=50_000)
        receiver = create_wallet(balance=0)
        transfer(sender, receiver, amount=50_000)
        assert sender.balance == 0
        assert receiver.balance == 50_000

    # Edge case: nilai tidak valid
    def test_transfer_fails_with_zero_amount(self):
        sender = create_wallet(balance=100_000)
        receiver = create_wallet(balance=0)
        with pytest.raises(InvalidAmountError):
            transfer(sender, receiver, amount=0)

    def test_transfer_fails_with_negative_amount(self):
        sender = create_wallet(balance=100_000)
        receiver = create_wallet(balance=0)
        with pytest.raises(InvalidAmountError):
            transfer(sender, receiver, amount=-10_000)

    # Edge case: transfer ke diri sendiri
    def test_transfer_to_self_fails(self):
        wallet = create_wallet(balance=100_000)
        with pytest.raises(SelfTransferError):
            transfer(wallet, wallet, amount=10_000)

Coverage: Metrik yang Sering Disalahpahami #

Coverage adalah indikator yang berguna tapi sering digunakan dengan cara yang salah. Coverage 90% tidak berarti kode sudah teruji dengan baik — ia hanya berarti 90% baris kode pernah dieksekusi oleh setidaknya satu test. Baris yang dieksekusi tanpa assertion yang bermakna tetap dihitung.

Masalah dengan mengejar angka coverage:

  Kode dengan coverage 95% tapi test tidak bermakna:
  def calculate_price(base_price, discount_pct):
      if discount_pct > 100:
          raise ValueError("Discount cannot exceed 100%")
      return base_price * (1 - discount_pct / 100)

  Test yang mengejar angka:
  def test_calculate_price():
      result = calculate_price(100, 10)
      assert result is not None  # ← coverage tercapai, tapi tidak ada nilai

  Test yang benar-benar berguna:
  def test_calculate_price_with_discount():
      assert calculate_price(100, 10) == 90.0   # 10% discount

  def test_calculate_price_with_zero_discount():
      assert calculate_price(100, 0) == 100.0   # tanpa discount

  def test_calculate_price_rejects_excessive_discount():
      with pytest.raises(ValueError):
          calculate_price(100, 110)             # > 100% tidak valid

Pendekatan yang lebih berguna dari mengejar threshold coverage adalah mengidentifikasi critical path dan memastikan path tersebut tercover dengan test yang bermakna.

Prioritas coverage yang benar:

  Level 1 — Wajib tercover dengan test bermakna:
  → Business logic inti (kalkulasi harga, validasi, state machine)
  → Security boundary (autentikasi, otorisasi, validasi input)
  → Error handling yang kritis (payment failure, data corruption prevention)
  → Edge case yang bisa menyebabkan data inconsistency

  Level 2 — Sebaiknya tercover:
  → Happy path semua endpoint/fungsi utama
  → Transformasi data yang kompleks

  Level 3 — Nice to have:
  → Utility functions sederhana
  → Generated code

  Yang tidak perlu ditest secara ketat:
  → Getter/setter trivial
  → Framework boilerplate
  → Third-party library code
Jangan biarkan threshold coverage menjadi target yang mematikan kreativitas. Tim yang terpaksa mencapai 90% coverage akan menulis test palsu untuk memenuhi angka itu. Lebih baik threshold yang rendah dengan test yang bermakna daripada threshold tinggi dengan test yang tidak menangkap bug apapun.

Test yang Cepat dan Deterministik #

Unit test sebagai CI guard hanya efektif jika developer benar-benar mau menunggu hasilnya. Test yang lambat atau tidak deterministik membuat developer frustrasi dan akhirnya mencari cara untuk melewatinya.

Karakteristik unit test yang efektif sebagai guard:

  ✓ Cepat — setiap test selesai dalam milidetik, seluruh suite dalam detik
    → Tidak ada koneksi ke database nyata
    → Tidak ada HTTP call ke service eksternal
    → Tidak ada I/O file yang tidak perlu
    → Gunakan in-memory implementations atau test doubles

  ✓ Deterministik — hasil sama setiap kali dijalankan
    → Tidak tergantung waktu saat ini (mock time jika perlu)
    → Tidak tergantung state yang tersisa dari test sebelumnya
    → Tidak tergantung urutan eksekusi test
    → Tidak tergantung random number tanpa seed yang fixed

  ✓ Isolated — setiap test berdiri sendiri
    → Setup dan teardown yang bersih untuk setiap test
    → Tidak ada shared state antar test
    → Test bisa dijalankan dalam urutan apapun dengan hasil yang sama

  ✗ Test yang bermasalah sebagai guard:
    → Test yang kadang lulus, kadang gagal (flaky test)
    → Test yang hanya bisa dijalankan di environment tertentu
    → Test yang butuh 5 menit untuk selesai
    → Test yang tidak bisa dijalankan secara paralel
# ANTI-PATTERN: test yang tidak deterministik
def test_send_notification():
    # Bergantung pada waktu nyata — bisa gagal jika dijalankan tepat tengah malam
    now = datetime.now()
    notification = create_notification(scheduled_at=now)
    assert notification.should_send_now()

# BENAR: kontrol waktu agar test deterministik
def test_send_notification_when_scheduled_time_reached():
    fixed_time = datetime(2025, 6, 1, 14, 0, 0)
    with freeze_time(fixed_time):  # atau inject mock clock
        notification = create_notification(scheduled_at=fixed_time)
        assert notification.should_send_now() == True

def test_do_not_send_notification_before_scheduled_time():
    scheduled_at = datetime(2025, 6, 1, 14, 0, 0)
    earlier_time = datetime(2025, 6, 1, 13, 59, 59)
    with freeze_time(earlier_time):
        notification = create_notification(scheduled_at=scheduled_at)
        assert notification.should_send_now() == False

Menata CI Pipeline sebagai Guard #

Guard tidak bekerja jika tidak dikonfigurasi dengan benar di level infrastruktur. Berikut bagaimana CI pipeline yang efektif sebagai guard seharusnya ditata.

flowchart TD
    A[Push ke branch PR] --> B[CI Pipeline dimulai]

    B --> C1[Linting & Formatting]
    B --> C2[Unit Test Suite]
    B --> C3[Security Scan]

    C1 --> D{Semua check lulus?}
    C2 --> D
    C3 --> D

    D -->|Tidak| E[Merge diblokir\nStatus check gagal]
    E --> F[Developer fix\nberdasarkan feedback CI]
    F --> A

    D -->|Ya| G[PR siap untuk\nhuman review]
    G --> H[Code Review]
    H -->|Lulus| I[Merge ke main]

    subgraph "Optional — berjalan paralel atau setelah merge"
        J[Integration Test]
        K[E2E Test]
        L[Performance Test]
    end

    I --> J
    I --> K
    I --> L

    style E fill:#ffcccc
    style I fill:#ccffcc
# Contoh GitHub Actions workflow sebagai guard
name: PR Guard

on:
  pull_request:
    branches: [main, develop]

jobs:
  unit-test:
    name: Unit Tests
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup environment
        uses: actions/setup-go@v4  # atau sesuaikan dengan bahasa yang digunakan
        with:
          go-version: '1.22'

      - name: Install dependencies
        run: go mod download

      - name: Run linting
        run: golangci-lint run ./...

      - name: Run unit tests
        run: go test -race -timeout 120s ./...
        # -race: deteksi race condition
        # -timeout: test tidak boleh berjalan lebih dari 2 menit

      - name: Check coverage
        run: |
          go test -coverprofile=coverage.out ./...
          go tool cover -func=coverage.out | grep total | awk '{print $3}' | \
            awk -F'%' '{if ($1 < 60) {print "Coverage " $1 "% below threshold"; exit 1}}'          
        # Threshold 60% sebagai minimum, bukan target

# Branch protection rules di GitHub:
# Settings → Branches → main → Require status checks to pass before merging
# → Tambahkan: "Unit Tests" sebagai required check
# → Enable: "Require branches to be up to date before merging"

Test sebagai Dokumentasi yang Executable #

Salah satu nilai unit test yang sering diremehkan adalah nilainya sebagai dokumentasi. Test yang ditulis dengan baik menjelaskan kepada engineer berikutnya — yang mungkin tidak punya konteks apapun tentang mengapa sebuah logika ditulis seperti itu — apa yang seharusnya terjadi dalam berbagai kondisi.

# Test sebagai dokumentasi yang buruk
def test_discount():
    assert apply_discount(100, "VIP") == 80

# Test sebagai dokumentasi yang baik
class TestApplyDiscount:
    """
    Sistem diskon mengikuti aturan berikut:
    - Member VIP mendapat 20% diskon
    - Member reguler mendapat 5% diskon
    - Non-member tidak mendapat diskon
    - Diskon tidak berlaku untuk produk kategori 'no_discount'
    """

    def test_vip_member_gets_20_percent_discount(self):
        # VIP member rate adalah 20% berdasarkan perjanjian dengan program loyalty
        base_price = 100_000
        expected_price = 80_000  # 100.000 - 20%

        result = apply_discount(base_price, member_type="VIP")

        assert result == expected_price

    def test_regular_member_gets_5_percent_discount(self):
        base_price = 100_000
        expected_price = 95_000  # 100.000 - 5%

        result = apply_discount(base_price, member_type="REGULAR")

        assert result == expected_price

    def test_no_discount_for_non_member(self):
        # Non-member tidak mendapat diskon apapun
        base_price = 100_000

        result = apply_discount(base_price, member_type=None)

        assert result == base_price

    def test_discount_not_applied_to_restricted_products(self):
        # Produk tertentu (misal: emas, pulsa) tidak boleh didiskon
        # Requirement dari compliance team — lihat RFC-089
        base_price = 100_000

        result = apply_discount(
            base_price,
            member_type="VIP",
            product_category="no_discount"
        )

        assert result == base_price  # harga tidak berubah meski VIP

Ketika engineer membaca test ini enam bulan kemudian, mereka langsung tahu: ada aturan bahwa produk kategori no_discount dikecualikan, dan ini ada alasannya (compliance). Tanpa test yang deskriptif ini, mereka harus menggali git history atau bertanya ke orang yang mungkin sudah tidak ada di tim.


Membangun Budaya Test-First dalam Konteks PR #

Tool dan CI pipeline bisa memaksa test untuk ada, tapi tidak bisa memaksa test itu bermakna. Budaya yang mendorong test-first — di mana test dilihat sebagai investasi, bukan beban — harus dibangun secara aktif.

Cara membangun budaya test-first:

  1. Reviewer mempertanyakan test yang tidak bermakna
     → "Test ini akan gagal jika logika discountnya diubah?"
     → "Ada test untuk kasus ketika input kosong?"
     Ini mengirim sinyal bahwa kualitas test sama pentingnya
     dengan kualitas kode.

  2. Bug fix selalu disertai test yang mereproduksi bug
     → Sebelum fix: tulis test yang gagal karena bug
     → Setelah fix: test harus lulus
     → Test ini menjaga agar bug tidak kembali

  3. Normalisasi TDD untuk logika kritis
     → Write test first, then implementation
     → Ini memaksa developer berpikir tentang behavior sebelum implementasi
     → Menghasilkan interface yang lebih testable secara natural

  4. Jadikan test yang lambat atau flaky sebagai masalah tim
     → Flaky test = prioritas untuk diperbaiki, bukan diabaikan
     → Test lambat = dioptimasi, bukan di-skip
     → Ini menjaga CI pipeline tetap dapat dipercaya

  5. Celebrate ketika test menangkap bug di CI
     → "Test kita menangkap regression sebelum ke produksi"
     adalah cerita sukses yang perlu dibagikan
     → Ini membangun pemahaman kolektif tentang nilai test

Anti-Pattern yang Harus Dihindari #

# ✗ Anti-pattern 1: test tanpa assertion yang bermakna
def test_create_user():
    user = create_user("[email protected]", "Ali")
    assert user is not None  # ← ini selalu true jika tidak ada exception

# ✓ Benar:
def test_create_user_with_valid_data():
    user = create_user("[email protected]", "Ali")
    assert user.email == "[email protected]"
    assert user.name == "Ali"
    assert user.id is not None
    assert user.created_at is not None

────────────────────────────────────────────────────────────────────────────

# ✗ Anti-pattern 2: satu test untuk semua skenario
def test_payment():
    # Test sukses
    result = process_payment(100000)
    assert result.success == True

    # Test gagal — di dalam test yang sama!
    result = process_payment(-1)
    assert result.success == False

    # Test edge case
    result = process_payment(0)
    assert result.success == False

# ✓ Benar: satu test, satu skenario
def test_payment_succeeds_with_valid_amount():
    result = process_payment(amount=100_000)
    assert result.success == True
    assert result.transaction_id is not None

def test_payment_fails_with_negative_amount():
    result = process_payment(amount=-1)
    assert result.success == False
    assert result.error == "INVALID_AMOUNT"

────────────────────────────────────────────────────────────────────────────

# ✗ Anti-pattern 3: test yang tergantung pada state eksternal
def test_get_user():
    # Bergantung pada data yang mungkin ada atau tidak ada di database test
    user = get_user_by_email("[email protected]")
    assert user.name == "Test User"

# ✓ Benar: buat data yang dibutuhkan di dalam test itu sendiri
def test_get_user_by_email():
    # Arrange: buat user yang dibutuhkan
    created_user = create_test_user(email="[email protected]", name="Test User")

    # Act
    found_user = get_user_by_email("[email protected]")

    # Assert
    assert found_user.id == created_user.id
    assert found_user.name == "Test User"

    # Cleanup: hapus data test (atau gunakan transactional test)

────────────────────────────────────────────────────────────────────────────

# ✗ Anti-pattern 4: skip test karena "nanti diperbaiki"
@pytest.mark.skip(reason="TODO: fix this test")
def test_critical_payment_logic():
    ...

# ✓ Benar: test yang di-skip harus menjadi prioritas untuk diperbaiki
# Jika test perlu di-skip sementara, buat tiket dan cantumkan nomor tiket
@pytest.mark.skip(reason="JIRA-789: flaky karena race condition di mock, fix dalam sprint ini")
def test_concurrent_payment():
    ...

Checklist Unit Test sebagai Guard #

KONFIGURASI CI GUARD:
  □ CI pipeline berjalan otomatis untuk setiap push ke PR branch
  □ Merge ke main branch diblokir jika CI gagal (branch protection rules)
  □ Unit test berjalan dalam waktu yang reasonable (< 5 menit untuk seluruh suite)
  □ Linting dan formatting dijalankan sebagai bagian dari CI guard

KUALITAS TEST:
  □ Setiap test memiliki assertion yang bermakna — bisa gagal jika behavior berubah
  □ Setiap test fokus pada satu skenario (satu test, satu case)
  □ Test menggunakan nama yang deskriptif (nama fungsi = apa yang ditest)
  □ Menggunakan AAA pattern: Arrange, Act, Assert
  □ Happy path dan edge case utama sudah tercover

  EDGE CASE YANG WAJIB DICEK:
  □ Input null/nil/empty
  □ Input di batas nilai (boundary values)
  □ Error path dan exception handling
  □ Concurrent access jika relevan

DETERMINISME DAN ISOLASI:
  □ Test tidak bergantung pada waktu nyata (gunakan mock clock jika perlu)
  □ Test tidak bergantung pada state database eksternal (gunakan in-memory atau fixtures)
  □ Test tidak bergantung pada urutan eksekusi
  □ Test tidak meninggalkan state yang mempengaruhi test lain

DALAM PR:
  □ PR dengan perubahan business logic disertai test yang mencakup perubahan itu
  □ Bug fix disertai test yang gagal sebelum fix dan lulus setelah fix
  □ Refactor: seluruh test suite tetap hijau (tidak ada behavior yang berubah)
  □ Tidak ada test yang di-skip tanpa alasan dan nomor tiket yang jelas

Ringkasan #

  • Unit test sebagai guard adalah shift-left quality — bug yang ditangkap di CI sebelum code review jauh lebih murah dari bug yang ditemukan reviewer, atau lebih buruk, di produksi.
  • Test dan reviewer saling melengkapi — reviewer tidak bisa mengeksekusi semua skenario di kepalanya. Test bisa. Keduanya diperlukan: test membuktikan behavior, reviewer menilai desain.
  • Test yang bisa gagal lebih berharga dari test yang selalu lulus — test yang tidak punya assertion bermakna, atau yang hanya mengecek bahwa fungsi tidak throw exception, tidak melindungi apapun.
  • Coverage adalah indikator, bukan tujuan — 80% coverage dengan test yang tidak bermakna lebih berbahaya dari 50% coverage dengan test yang sungguh-sungguh memverifikasi behavior kritis.
  • Critical path harus tercover dengan test yang bermakna — business logic, security boundary, error handling yang kritis, dan edge case yang bisa menyebabkan inkonsistensi data.
  • Test harus cepat dan deterministik — guard yang lambat atau flaky akan dicari cara untuk dilewati. Test yang tidak bisa dipercaya lebih buruk dari tidak ada test.
  • Satu test, satu skenario — test yang menguji banyak hal sekaligus sulit dibaca dan sulit di-debug saat gagal. Buat satu test per skenario dengan nama yang deskriptif.
  • Test adalah dokumentasi executable — test yang ditulis dengan baik menjelaskan kepada engineer berikutnya apa yang seharusnya terjadi dan mengapa, tanpa perlu membaca kode implementasi.
  • Bug fix harus selalu disertai test — tulis test yang gagal karena bug, perbaiki bug, pastikan test lulus. Test ini menjaga agar bug tidak kembali di masa depan.
  • Budaya dibangun dari code review — reviewer yang mempertanyakan kualitas test, bukan hanya keberadaannya, membangun pemahaman kolektif bahwa test yang bermakna adalah standar, bukan opsional.

← Sebelumnya: Code Review Ethics   Berikutnya: API: Fundamental →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact