A detailed backend reference

Pengujian otomatis, bukti bahwa kode Anda melakukan yang Anda kira.

Versi Sederhana

Penelusuran dari prinsip dasar tentang cara engineer backend memverifikasi sistem mereka: dari satu tes unit dan kosakata test-double, lewat tes integrasi terhadap database dan handler HTTP nyata, tes kontrak antar layanan, pemeriksaan end-to-end, coverage dan flakiness, sampai TDD, load testing, dan tes di pipeline deployment. Ditulis bukan hanya menyebut apa fungsi tiap jenis tes tapi kenapa ia ada dan bagaimana menulisnya dengan baik. Contoh di Go (testing) dan Python (pytest).

Unit / Integration / E2E Go testing / pytest Go 1.22+ / Python 3.11+ 21 sections
Part I / Why & What
01

Kenapa Tes Ada

Alasan jujur menulis tes otomatis bukan "kebenaran" secara abstrak, melainkan kepercayaan diri untuk mengubah kode tanpa rasa takut. Sistem tanpa tes mengapur: setiap edit berisiko memecah sesuatu yang tak terlihat, jadi orang berhenti refactor, berhenti upgrade dependensi, dan memutar mengelilingi bagian yang menakutkan. Sistem yang teruji baik tetap lunak, Anda bisa menyusun ulang secara agresif karena suite tes hijau memberi tahu Anda, dalam hitungan detik, bahwa perilaku tetap berlaku. Tes lah yang membuat codebase terus berevolusi alih-alih membusuk.

Tes juga spesifikasi yang bisa dieksekusi. Dokumen prosa menyimpang dari kenyataan (masalah yang sama yang dicatat bab gRPC soal kontrak REST); tes yang menegaskan Divide(10, 2) == 5 tidak bisa bohong, ia entah lulus terhadap kode saat ini atau tidak. Baca suite tes yang baik dan Anda mempelajari persis apa yang dijanjikan kode, dengan contoh. Dan tes adalah jaring regresi: begitu Anda menulis tes untuk sebuah bug, bug itu tidak akan pernah diam-diam kembali, jaring menangkapnya di setiap run masa depan.

Manual testing is checking your work by re-reading it once. Automated testing is hiring a tireless proofreader who re-checks the entire document, instantly, every time you change a single word, and never gets bored or misses a line.
The economics
A bug costs more the later you catch it
cost to fix -> while coding$ unit test (CI)$ code review / staging$$$ in production$$$$$$ Automated tests shift detection left, to the cheapest possible moment, on every change.
The entire value proposition: catch the defect seconds after you write it, not days later after it has shipped to users and corrupted data.
What tests really buy you

Kepercayaan untuk berubah (refactor tanpa takut), spesifikasi yang bisa dieksekusi (dokumen yang tidak bisa menyimpang), perlindungan regresi (bug yang diperbaiki tetap diperbaiki), dan umpan balik lebih cepat (detik, bukan klik manual). Kebenaran efek samping; kemampuan berubah hadiahnya.

02

Piramida Pengujian

Tidak semua tes setara, dan keputusan strategis terpenting campurannya. Piramida pengujian heuristik pemandu: punya banyak tes unit cepat murah di dasar, lebih sedikit tes integrasi di tengah, dan sangat sedikit tes end-to-end lambat mahal di puncak. Bentuknya berasal dari kompromi yang mengalir di seluruh manual ini: saat Anda naik, tes makin realistis (ia menggerakkan lebih banyak sistem nyata) tapi juga lebih lambat, lebih rapuh, dan lebih sulit di-debug.

The pyramid
Many fast unit tests, fewer integration, a thin layer of E2E
E2E few / slow Integration some / medium Unit many / fast / isolated slower / more realistic -> more tests ->
Lean on the base. Each layer up costs more to write, run, and maintain, so reserve the scarce, expensive top for the few flows that truly need full-system confidence.
LapisanTesKecepatanRealismePakai untuk
UnitSatu unit, dependensi dipalsukanMilidetikRendahLogika, kasus tepi, cabang
IntegrasiBeberapa unit + deps nyata (DB, cache)DetikSedangWiring: query, handler, serialisasi
E2ESeluruh sistem lewat antarmuka nyatanyaDetik-menitTinggiHanya perjalanan pengguna kritis
The ice-cream-cone anti-pattern

Balik piramidanya, banyak tes E2E lambat, sedikit tes unit, dan Anda dapat "ice-cream cone": suite yang makan waktu lama berjalan, gagal sesekali (bagian 14), dan memberi sinyal samar (kegagalan bisa di mana saja di stack). Tim di jebakan ini berhenti memercayai suite dan berhenti menjalankannya. Dorong detail ke tes unit cepat; pakai E2E hemat untuk kepercayaan bahwa bagian-bagiannya tersambung.

03

Anatomi Sebuah Tes

Setiap tes yang baik, di setiap lapisan, punya struktur tiga ketukan yang sama, Arrange, Act, Assert (AAA; dunia BDD menyebutnya Given-When-Then). Arrange menyiapkan dunia dan input; Act melakukan satu operasi yang diuji; Assert memeriksa hasilnya sesuai harapan. Menjaga fase-fase ini terpisah dan menguji satu perilaku per tes membuat kegagalan langsung terbaca: Anda tahu persis perilaku mana yang rusak.

Arrange / Act / Assert
Three phases, one behavior under test
1 / Arrangebuild inputs & doubles 2 / Actcall the unit ONCE 3 / Assertcheck the outcome One behavior per test / a descriptive name / a clear failure message.
// File naming convention: foo.go is tested by foo_test.go in the same package.
// Test functions are func TestXxx(t *testing.T), the `go test` tool finds them.
func TestDiscount_AppliesPercentage(t *testing.T) {
    // Arrange, set up inputs (and any doubles)
    cart := Cart{Subtotal: 200}
    coupon := Coupon{Percent: 10}

    // Act, the ONE operation under test
    total := ApplyDiscount(cart, coupon)

    // Assert, state the expectation; a good message explains the failure
    if total != 180 {
        t.Errorf("ApplyDiscount(200, 10%%) = %d; want 180", total)
    }
}
# pytest discovers files named test_*.py and functions named test_*.
# A bare `assert` is enough, pytest rewrites it to show a rich failure diff.
def test_discount_applies_percentage():
    # Arrange, set up inputs (and any doubles)
    cart = Cart(subtotal=200)
    coupon = Coupon(percent=10)

    # Act, the ONE operation under test
    total = apply_discount(cart, coupon)

    # Assert, pytest prints actual vs expected automatically on failure
    assert total == 180

Apa yang membuat assertion baik

  • Uji perilaku, bukan implementasi. Tegaskan hasil yang teramati (diskon diterapkan), bukan mekanik internal (helper privat ini dipanggil). Tes yang terkopel implementasi pecah di setiap refactor dan mengalahkan seluruh tujuannya (bagian 1, bagian 20).
  • Satu alasan gagal. Tes yang menegaskan lima hal tak berhubungan memberi tahu sedikit saat ia merah. Prefer tes terfokus dengan nama deskriptif, namanya harus terbaca seperti kalimat tentang perilakunya.
  • Deterministik. Input sama, hasil sama, setiap run, urutan apa pun, sifat yang dilindungi bagian 14.
The standard tools

Manual ini memakai stack utama tiap bahasa: paket testing bawaan Go dengan go test (tanpa framework; library standar adalah framework-nya), dan pytest Python, standar de facto dengan fixture, parametrize, dan penulisan-ulang assert polos. Keduanya yang akan Anda temui di repo backend nyata.

Part II / Unit Tests
04

Tes Unit

Unit test memverifikasi satu potongan logika kecil, fungsi atau method, terisolasi dari sisa sistem. Tanpa database, tanpa jaringan, tanpa file system, tanpa jam: hanya input masuk, output diperiksa. Isolasi itulah yang membuatnya cepat (ribuan berjalan dalam detik) dan presisi (kegagalan menunjuk tepat satu unit). Mereka dasar piramida yang lebar (bagian 2) dan tempat sebagian besar coverage kasus tepi dan cabang Anda harus tinggal.

Target alami tes unit adalah logika domain/bisnis Anda: kalkulasi dan keputusan murni yang secara inheren tidak butuh I/O: aturan harga, validasi, transisi state, parsing. (Ini memetakan langsung ke lapisan service dari bab controllers-services-repositories; mendorong logika ke fungsi murni di sana persis yang membuatnya bisa di-unit-test di sini.)

Isolation
A unit test exercises one piece; real dependencies are kept out
inputs unit under testpure logic assert output ✗ no DB✗ no network✗ no real clock
// The unit: pure logic, no I/O, trivially testable.
func IsStrongPassword(p string) bool {
    if len(p) < 8 { return false }
    var hasDigit, hasUpper bool
    for _, r := range p {
        if r >= '0' && r <= '9' { hasDigit = true }
        if r >= 'A' && r <= 'Z' { hasUpper = true }
    }
    return hasDigit && hasUpper
}

func TestIsStrongPassword(t *testing.T) {
    if IsStrongPassword("short1A") {            // 7 chars -> too short
        t.Error("expected short password to be rejected")
    }
    if !IsStrongPassword("longEnough9") {        // meets all rules
        t.Error("expected valid password to be accepted")
    }
}

// Run:  go test ./...        (-v for verbose, -run TestIsStrong to filter)
# The unit: pure logic, no I/O, trivially testable.
def is_strong_password(p: str) -> bool:
    if len(p) < 8:
        return False
    return any(c.isdigit() for c in p) and any(c.isupper() for c in p)

def test_rejects_short_password():
    assert is_strong_password("short1A") is False    # 7 chars -> too short

def test_accepts_valid_password():
    assert is_strong_password("longEnough9") is True  # meets all rules

# Run:  pytest            (-v verbose, -k strong to filter by name)
If a unit is hard to test, the design is the smell

Saat sebuah fungsi menyakitkan untuk di-unit-test, ia menjangkau global, membuka koneksi, membaca jam, atau melakukan sepuluh hal, tes itu memberi tahu Anda kodenya difaktorkan buruk, bukan bahwa testing itu sulit. Perbaikannya hampir selalu memisahkan logika murni dari I/O dan menyuntikkan dependensi (bagian 6). Testability dan desain yang baik adalah properti yang sama dilihat dari dua sudut.

05

Test Double

Unit nyata bergantung pada hal lain, repository, payment gateway, pengirim email. Untuk menguji unit dalam isolasi Anda mengganti kolaborator itu dengan test double: pengganti yang Anda kendalikan. "Mock" dipakai longgar dalam percakapan, tapi kosakata presisi (dari Gerard Meszaros / Martin Fowler) membedakan lima jenis, dan mengetahui perbedaannya mempertajam cara Anda menguji.

DoubleApa ituAnda memakainya untuk...
DummyPlaceholder yang diteruskan tapi tidak pernah dipakaimemenuhi parameter wajib
StubMengembalikan jawaban siap untuk panggilanmemberi unit input tetap (mis. "DB mengembalikan user ini")
SpyStub yang juga mencatat cara ia dipanggilmenegaskan belakangan bahwa sesuatu dipanggil, dan bagaimana
MockDiprogram di muka dengan ekspektasi yang ia verifikasimenegaskan interaksi terjadi (mis. "email dikirim sekali")
FakeImplementasi ringan yang berfungsimengganti dependensi nyata dengan murah (mis. DB di memori)

Verifikasi state vs verifikasi interaksi

Perbedaan terdalam: stub/fake mendukung verifikasi state (Anda menegaskan hasilnya), sementara mock/spy mendukung verifikasi interaksi (Anda menegaskan panggilan terjadi). Prefer verifikasi state, ia kokoh terhadap refactoring. Rengkuh verifikasi interaksi hanya saat interaksi adalah perilakunya (email benar-benar harus dikirim, tagihan harus diterbitkan tepat sekali). Over-mocking interaksi penyebab utama tes rapuh (bagian 20).

// The unit depends on an interface, the seam that lets us substitute a double (sec 6).
type UserRepo interface{ FindByID(id string) (*User, error) }

type Notifier interface{ Send(to, msg string) error }

func Greet(repo UserRepo, n Notifier, id string) error {
    u, err := repo.FindByID(id)        // collaborator 1
    if err != nil { return err }
    return n.Send(u.Email, "Hi "+u.Name) // collaborator 2 (interaction we care about)
}

// STUB: returns a canned user. SPY: also records what Send received.
type stubRepo struct{ user *User }
func (s stubRepo) FindByID(string) (*User, error) { return s.user, nil }

type spyNotifier struct{ calls int; lastTo string }
func (s *spyNotifier) Send(to, msg string) error { s.calls++; s.lastTo = to; return nil }

func TestGreet_SendsEmail(t *testing.T) {
    repo := stubRepo{user: &User{Email: "a@x.com", Name: "Ada"}} // arrange (stub)
    spy := &spyNotifier{}

    _ = Greet(repo, spy, "u1")                                   // act

    if spy.calls != 1 || spy.lastTo != "a@x.com" {               // assert the interaction
        t.Errorf("expected one email to a@x.com, got %d to %q", spy.calls, spy.lastTo)
    }
}
from unittest.mock import Mock

# The unit depends on injected collaborators (sec 6).
def greet(repo, notifier, user_id):
    user = repo.find_by_id(user_id)          # collaborator 1
    notifier.send(user.email, f"Hi {user.name}")  # collaborator 2 (the interaction)

def test_greet_sends_email():
    # STUB: a Mock configured to return a canned user
    repo = Mock()
    repo.find_by_id.return_value = User(email="a@x.com", name="Ada")
    notifier = Mock()                        # will record calls (acts as spy/mock)

    greet(repo, notifier, "u1")              # act

    # assert the INTERACTION happened exactly as expected
    notifier.send.assert_called_once_with("a@x.com", "Hi Ada")

Go cenderung menulis double kecil manual terhadap interface (eksplisit, tanpa sihir); unittest.mock Python membuat double fleksibel yang keduanya stub return dan mencatat panggilan. Konsep sama, ergonomi berbeda.

Don't mock what you don't own

Hindari mocking library pihak ketiga atau API HTTP eksternal langsung, mock Anda meng-encode asumsi Anda tentang perilaku mereka, yang bisa salah, jadi tes lulus sementara produksi rusak. Gantinya bungkus dependensi di balik interface Anda sendiri dan mock itu, atau pakai fake/tes kontrak (bagian 11, bagian 17). Mock seam Anda sendiri, bukan internal orang lain.

06

Dependency Injection demi Testability

Teknik tunggal yang membuat unit testing mungkin adalah dependency injection (DI): alih-alih unit membuat atau meraih kolaboratornya, ia menerimanya dari luar. Titik serah itu seam: tempat tes bisa menyelipkan double (bagian 5). Tanpa seam, tanpa isolasi. Inilah kenapa arsitektur controllers-services-repositories bersikeras meneruskan dependensi ke bawah: itu bukan seremoni, itulah yang menjaga kode tetap testable.

The seam
Hard-coded dependency vs injected dependency
hard-coded, untestable Servicedb := openRealPostgres() real Postgres only no way to substitute -> needs a real DB to test injected, testable Service(repo Repo)uses the interface real DB (production) fake/in-memory (tests) same interface -> swap a fake in tests, real in prod
// Depend on an INTERFACE, not a concrete type, that interface is the seam.
type OrderRepo interface{ Save(o Order) error }

type OrderService struct{ repo OrderRepo } // injected, not constructed inside

func NewOrderService(r OrderRepo) *OrderService { return &OrderService{repo: r} }

func (s *OrderService) Place(o Order) error {
    if o.Total <= 0 { return errors.New("invalid total") } // pure logic, easily tested
    return s.repo.Save(o)
}

// In tests: a FAKE in-memory repo (no real DB needed)
type fakeRepo struct{ saved []Order }
func (f *fakeRepo) Save(o Order) error { f.saved = append(f.saved, o); return nil }

func TestPlace_RejectsInvalidTotal(t *testing.T) {
    svc := NewOrderService(&fakeRepo{})
    if err := svc.Place(Order{Total: 0}); err == nil {
        t.Error("expected error for non-positive total")
    }
}
// In production:  NewOrderService(postgresRepo), same code, real dependency.
# Accept the collaborator as a constructor argument, that parameter is the seam.
class OrderService:
    def __init__(self, repo):        # injected, not created inside
        self.repo = repo

    def place(self, order):
        if order.total <= 0:
            raise ValueError("invalid total")   # pure logic, easily tested
        self.repo.save(order)

# In tests: a FAKE in-memory repo (no real DB needed)
class FakeRepo:
    def __init__(self): self.saved = []
    def save(self, order): self.saved.append(order)

def test_place_rejects_invalid_total():
    svc = OrderService(FakeRepo())
    with pytest.raises(ValueError):
        svc.place(Order(total=0))

# In production:  OrderService(postgres_repo), same code, real dependency.
Design for testability = good design

DI, interface kecil, dan memisahkan logika murni dari I/O bukan "trik tes", mereka prinsip yang sama yang membuat kode modular dan mudah dirawat. Codebase yang mudah diuji hampir menurut definisi terstruktur baik. Suite tes sinyal kualitas desain yang berkelanjutan.

07

Tes Table-Driven & Terparametri

Kebanyakan logika perlu diperiksa terhadap banyak pasangan input/output, happy path, kasus tepi, batas, kasus error. Copy-paste satu tes per kasus itu noise. Kedua bahasa punya idiom untuk "body tes sama, banyak kasus": table-driven test Go dan parametrize pytest. Di sinilah Anda mendapat coverage murah dan padat atas cabang dan tepi, kuda beban sesungguhnya lapisan unit.

func TestApplyDiscount(t *testing.T) {
    // The TABLE: each row is one case with a name.
    cases := []struct {
        name     string
        subtotal int
        percent  int
        want     int
    }{
        {"ten percent off 200", 200, 10, 180},
        {"zero discount",       100, 0,  100},
        {"full discount",       100, 100, 0},
        {"rounds down",          99, 10,  90}, // boundary/edge case
    }
    for _, c := range cases {
        // t.Run makes each row a named subtest, failures report the case name.
        t.Run(c.name, func(t *testing.T) {
            got := ApplyDiscount(Cart{Subtotal: c.subtotal}, Coupon{Percent: c.percent})
            if got != c.want {
                t.Errorf("got %d, want %d", got, c.want)
            }
        })
    }
}
// `go test -run TestApplyDiscount/rounds_down` runs just one case.
import pytest

# Each tuple is one case; ids give readable names in the output.
@pytest.mark.parametrize(
    "subtotal, percent, expected",
    [
        (200, 10, 180),
        (100, 0,  100),
        (100, 100, 0),
        (99,  10, 90),     # boundary/edge case
    ],
    ids=["ten-off-200", "zero", "full", "rounds-down"],
)
def test_apply_discount(subtotal, percent, expected):
    got = apply_discount(Cart(subtotal=subtotal), Coupon(percent=percent))
    assert got == expected

# `pytest -k rounds-down` runs just one case; each shows up as its own test.
Name every case

Pembayaran idiom ini bukan hanya lebih sedikit kode, tapi tiap kasus menjadi tes bernama independen, bisa dijalankan independen. Saat satu rusak, laporan menyebut kasus persisnya ("pembulatan-bawah gagal"), dan Anda bisa menjalankan ulang hanya itu. Selalu beri kasus nama deskriptif; baris anonim yang gagal hanya berbunyi "kasus 3," yang tidak membantu siapa pun.

Part III / Integration Tests
08

Tes Integrasi

Tes unit membuktikan tiap bagian bekerja sendirian; tes integrasi membuktikan bagian-bagian bekerja bersama, dan yang penting, bahwa mereka berintegrasi benar dengan sistem eksternal nyata yang Anda mock di lapisan unit: database, cache, message broker, layanan lain. Mereka duduk di tengah piramida (bagian 2): lebih lambat dari tes unit karena I/O nyata terlibat, tapi menangkap kategori bug yang secara struktural tidak bisa ditangkap tes unit.

What only integration tests catch

Query SQL dengan kolom typo, mapping ORM yang tidak cocok skema, ketidakcocokan serialisasi antar layanan, migrasi yang tidak jalan, connection string salah, transaksi yang tidak commit. Setiap satu dari ini hidup di seam antara kode Anda dan sistem nyata, persis seam yang di-stub tes unit. Mocking DB tidak bisa mengungkap bahwa query Anda SQL tidak valid; hanya menjalankannya terhadap database nyata yang bisa.

Wider scope
An integration test exercises several units plus a real dependency
under test (real, wired together) handler service repository REAL Postgresin a container (sec 9) Send a request in, assert what comes back and what landed in the database.
Keep them isolated & repeatable

Tes integrasi menyentuh state bersama, jadi rawan flakiness (bagian 14) jika mereka bocor satu sama lain. Tiap tes harus mulai dari state yang dikenal dan membersihkan diri, skema segar, transaksi di-rollback, atau tabel di-truncate (bagian 9). Tes yang bergantung pada database bersama yang di-seed manual, atau pada berjalan dalam urutan tertentu, akhirnya mengkhianati Anda.

09

Menguji dengan Database Nyata

Pertanyaan berulang: bagaimana mendapat database "nyata" dalam tes tanpa bergantung pada server bersama yang dirawat manual? Jawaban modern Testcontainers: library yang menyalakan database sungguhan dalam container Docker sekali pakai (container bab 21) saat tes, memberi Anda connection string-nya, dan merobohkannya setelahnya. Anda menguji terhadap engine aktual (versi Postgres sama dengan prod), terisolasi penuh, tanpa apa pun yang diinstal. Untuk menjaga tes independen satu sama lain, dua taktik standar transaction rollback (bungkus tiap tes dalam transaksi, roll back di akhir) atau truncate tabel antar tes.

// Build tag keeps slow integration tests out of the fast unit run:
//   //go:build integration      -> run with:  go test -tags=integration ./...
func TestUserRepo_SaveAndFind(t *testing.T) {
    ctx := context.Background()

    // Arrange: start a REAL Postgres in a throwaway container (testcontainers-go)
    pg, err := postgres.Run(ctx, "postgres:16",
        postgres.WithDatabase("app"), postgres.WithUsername("u"), postgres.WithPassword("p"))
    if err != nil { t.Fatal(err) }
    t.Cleanup(func() { pg.Terminate(ctx) }) // torn down automatically after the test

    dsn, _ := pg.ConnectionString(ctx, "sslmode=disable")
    db, _ := sql.Open("postgres", dsn)
    runMigrations(t, db)                    // apply the same schema as production

    repo := NewUserRepo(db)

    // Act: exercise the REAL query path
    _ = repo.Save(User{ID: "u1", Email: "a@x.com"})
    got, err := repo.FindByID("u1")

    // Assert: round-trips correctly through actual SQL
    if err != nil || got.Email != "a@x.com" {
        t.Fatalf("round-trip failed: %v / %+v", err, got)
    }
}
import pytest
from testcontainers.postgres import PostgresContainer

# A fixture spins up a REAL Postgres once per module, then tears it down.
@pytest.fixture(scope="module")
def db_url():
    with PostgresContainer("postgres:16") as pg:   # throwaway container
        url = pg.get_connection_url()
        run_migrations(url)                         # same schema as production
        yield url
        # container stopped automatically on exit

@pytest.fixture
def repo(db_url):
    # function-scoped: clean state per test (truncate / rollback)
    r = UserRepo(db_url)
    yield r
    r.truncate_all()                                # keep tests independent (sec 8)

@pytest.mark.integration            # marker: pytest -m integration
def test_save_and_find(repo):
    repo.save(User(id="u1", email="a@x.com"))       # act through REAL SQL
    got = repo.find_by_id("u1")
    assert got.email == "a@x.com"                   # round-trips correctly

Keduanya mengisolasikan tes lambat di balik flag/marker (build tag Go, marker pytest) supaya suite unit milidetik tetap cepat dan tes DB yang lebih berat berjalan saat diminta atau di CI (bagian 19).

Test against the real engine, not a substitute

Mengganti SQLite "karena cepat" saat produksi menjalankan Postgres jebakan klasik: dialek berbeda (tipe, constraint, JSON, upsert, locking), jadi tes bisa lulus di SQLite dan query yang sama gagal di Postgres. Testcontainers ada persis supaya Anda bisa memakai engine produksi aktual dengan murah, uji yang Anda kirim.

10

Menguji HTTP Handler & API

Handler Anda (bab HTTP dan REST) target integrasi utama: Anda ingin menembakkan request nyata ke stack routing + handler + serialisasi Anda dan menegaskan respons nyatanya, status code, header, body, tanpa mengikat socket atau menjalankan server lewat jaringan. Kedua ekosistem menyediakan test client dalam proses yang menggerakkan handler langsung: net/http/httptest Go dan TestClient yang dikirim FastAPI/Starlette (dan padanan Flask).

import (
    "net/http"
    "net/http/httptest"
    "strings"
    "testing"
)

func TestCreateUser_Returns201(t *testing.T) {
    handler := NewRouter(deps)  // the REAL router + handlers (deps may use a fake repo sec 6)

    // Arrange: build a request and an in-memory response recorder (no network)
    body := strings.NewReader(`{"email":"a@x.com","name":"Ada"}`)
    req := httptest.NewRequest(http.MethodPost, "/api/v1/users", body)
    req.Header.Set("Content-Type", "application/json")
    rec := httptest.NewRecorder()

    // Act: drive the handler directly
    handler.ServeHTTP(rec, req)

    // Assert: on the real response, status, then body
    if rec.Code != http.StatusCreated {
        t.Fatalf("status = %d; want 201; body=%s", rec.Code, rec.Body.String())
    }
    if ct := rec.Header().Get("Content-Type"); !strings.Contains(ct, "application/json") {
        t.Errorf("content-type = %q", ct)
    }
}
from fastapi.testclient import TestClient
from app import app   # the REAL FastAPI app (dependencies may be overridden with fakes sec 6)

client = TestClient(app)   # drives the app in-process, no network, no running server

def test_create_user_returns_201():
    # Act: fire a real request through routing + handler + serialization
    resp = client.post("/api/v1/users", json={"email": "a@x.com", "name": "Ada"})

    # Assert: on the real response, status, headers, body
    assert resp.status_code == 201
    assert resp.headers["content-type"].startswith("application/json")
    assert resp.json()["email"] == "a@x.com"

# FastAPI tip: override real dependencies with fakes via app.dependency_overrides
# so the handler runs but the repo is in-memory, an integration test of the web layer.
Choose your blast radius

Tes ini fleksibel: rangkai fake repository dan Anda meng-integration-test hanya lapisan web (routing, decoding, status code, amplop error) cepat dan deterministik; rangkai DB nyata via testcontainers (bagian 9) dan Anda punya tes integrasi lebih luas dari seluruh jalur request. Pilih cakupannya sengaja per tes, lebih sempit lebih cepat dan presisi; lebih lebar lebih realistis.

11

Contract Testing

Di dunia microservice (bab gRPC dan desain API), layanan jarang hidup sendiri, ia mengonsumsi yang lain dan dikonsumsi yang lain. Bahayanya: provider mengubah bentuk responsnya, semua tesnya sendiri lulus, tapi diam-diam memecah setiap consumer. Menguji end-to-end setiap kombinasi sangat lambat. Contract testing menyelesaikannya dengan memverifikasi kedua sisi setuju pada antarmukanya, tanpa menjalankannya bersama.

Consumer-driven contracts
The consumer's expectations become a contract the provider must satisfy
Consumer"I send X, expect Y" generates the Contractexpected req/resp pairs verified vs Provider"do I still return Y?" Each side is tested independently against the shared contract, no need to run both together. If the provider breaks the contract, its build fails, before it can break the consumer in prod.

Tool seperti Pact mengimplementasikan ini: tes consumer mencatat request yang ia buat dan respons yang ia harapkan ke file kontrak; CI provider memutar ulang kontrak itu terhadap provider nyata dan gagal jika tidak lagi berlaku. Untuk layanan gRPC/Protobuf, .proto plus deteksi perubahan yang memecah skema (tooling buf dari bab gRPC) memainkan peran serupa, skema adalah kontrak yang diperiksa mesin, dan CI menolak perubahan tak kompatibel.

Where contract tests fit

Mereka jalan tengah pragmatis antara "mock layanan lain dan berharap mock Anda akurat" (cepat tapi bisa menyimpang dari kenyataan, peringatan bagian 5) dan "nyalakan semuanya dan uji end-to-end" (realistis tapi lambat dan rapuh). Contract test memberi kepercayaan lintas-layanan di kecepatan tes unit, itulah kenapa ia berskala ke armada layanan besar tempat E2E penuh lintas semua layanan tidak praktis.

Part IV / E2E, Coverage & Reliability
12

Tes End-to-End

Tes end-to-end (E2E) menggerakkan sistem yang terakit penuh lewat antarmuka eksternal nyatanya, persis seperti pengguna atau klien, tanpa internal yang di-stub, semuanya berjalan: API, database, cache, layanan dependen. Ia menjawab satu pertanyaan yang tidak bisa dijawab lapisan lebih rendah: apakah semua bagian, dirangkai nyata, benar-benar mengirimkan hasil yang terlihat pengguna? Ia puncak piramida (bagian 2): realisme maksimum, biaya maksimum.

Full stack
A real client drives the whole running system
test client the REAL deployed system API DB cache svc B Yes Nothing faked, assert the user-visible outcome of a complete journey.
Use them sparingly, on purpose

Tes E2E lambat, mahal dirawat, dan paling rawan flake (bagian 14): gangguan di mana pun, jaringan, timing, dependensi, menggagalkannya, dan kegagalan tidak memberi tahu Anda di mana masalahnya. Jadi simpan E2E untuk segenggam perjalanan kritis ("user daftar -> login -> menempatkan order -> pembayaran sukses"). Jangan uji kasus tepi atau setiap cabang di sini, itu milik tes unit cepat (bagian 4). E2E mengonfirmasi sistem tersambung; lapisan lebih rendah mengonfirmasi ia benar.

Smoke tests & testing in production

Kerabat dekatnya smoke test: set kecil pemeriksaan E2E yang dijalankan tepat setelah deploy untuk mengonfirmasi sistem hidup ("apakah /health hijau? bisakah aku login?"), berpasangan alami dengan health probe dan rollout bab 21. Tim matang memperluasnya dengan testing in production: synthetic monitoring, analisis canary (bagian 20 di sana), dan feature flag, karena sebagian sifat hanya menampakkan diri di bawah lalu lintas nyata.

13

Test Coverage

Coverage mengukur berapa banyak kode Anda yang dieksekusi tes, biasanya persentase baris atau cabang yang dijalankan selama suite. Ia berguna sebagai senter untuk menemukan kode yang tidak diuji sama sekali, tapi sangat mudah salah dibaca sebagai ukuran kualitas tes. Ia bukan. Coverage memberi tahu apa yang dieksekusi, tidak pernah apa yang ditegaskan secara bermakna.

# Built into the toolchain, no extra dependency.
go test -cover ./...                       # prints % per package

go test -coverprofile=cover.out ./...      # write a detailed profile
go tool cover -func=cover.out              # per-function breakdown
go tool cover -html=cover.out              # open an annotated, line-by-line HTML view
# Via the coverage.py / pytest-cov plugin.
pip install pytest-cov

pytest --cov=app                           # coverage for the `app` package
pytest --cov=app --cov-report=term-missing # show exactly which lines are UNcovered
pytest --cov=app --cov-report=html         # browsable htmlcov/ report
pytest --cov=app --cov-branch              # branch coverage, not just line

Line vs branch coverage

Line coverage bertanya "apakah baris ini berjalan?" Branch coverage bertanya pertanyaan lebih sulit "apakah setiap arah dari setiap keputusan diambil?" Satu tes bisa memukul 100% baris dari sebuah if sementara hanya pernah mengambil cabang true, jalur else tidak tereksekusi dan tidak teruji meski line coverage tampak lengkap. Branch coverage angka yang lebih jujur; prefer itu.

Coverage is a floor, not a goal, and Goodhart bites

Anda bisa mencapai 100% coverage dengan tes yang menegaskan tidak ada apa pun, cukup panggil setiap fungsi dan periksa tidak ada exception. Tercover penuh, sama sekali tak bernilai. Begitu persentase coverage menjadi target, orang mempermainkannya dengan tes tanpa assertion (hukum Goodhart: ukuran yang menjadi target berhenti menjadi ukuran yang baik). Pakai coverage untuk menemukan area tak teruji yang layak perhatian; nilai kualitas dengan apakah tes menegaskan perilaku bermakna (bagian 3, bagian 20). Coverage tinggi dengan assertion lemah kepercayaan palsu.

14

Tes Flaky

Tes flaky lulus kadang dan gagal lain kali tanpa perubahan kode apa pun. Flakiness korosif: ia melatih tim mengabaikan build merah ("tinggal jalankan ulang"), dan begitu orang berhenti memercayai suite, suite mati, run hijau tidak lagi berarti apa-apa. Tes flaky boleh dibilang lebih buruk daripada tidak ada tes, karena ia menghabiskan waktu dan mengikis kepercayaan yang memberi nilai bagi tes (bagian 1).

Penyebab flakinessPerbaikan
Waktu / jam: "sekarang", timeout, sleep, batas tanggalSuntikkan jam; tegaskan rentang bukan instan persis (bagian 17). Jangan pernah sleep untuk "menunggu"; poll sebuah kondisi.
Ketergantungan urutan tes: satu tes mengandalkan sisa tes lainBuat tiap tes men-set up & tear down state-nya sendiri (bagian 9, bagian 15). Jalankan urutan acak untuk membuang yang begini.
State mutable bersama: global, baris DB bersama, singletonIsolasikan per tes; hindari state global; pakai fixture segar.
Konkurensi / race: goroutine, thread, timing asyncSinkronkan pada sinyal bukan timing; jalankan dengan race detector (go test -race).
Jaringan nyata / API eksternal: latensi, rate limit, downtimeMock batasnya (bagian 17); simpan panggilan nyata ke set E2E kecil yang toleran.
Data tak terurut: iterasi map, baris DB tanpa ORDER BYSortir sebelum membandingkan, atau tegaskan keanggotaan set bukan urutan.
The cure is determinism

Setiap tes flaky non-determinisme tersembunyi, waktu, urutan, konkurensi, atau state bersama yang bocor masuk. Perbaikannya selalu menghapus sumber keacakan: kendalikan jam, isolasi state, sinkronkan pada peristiwa alih-alih sleep, dan stub jaringan. Karantina tes flaky (tandai dan lewati dari run gating) hanya sebagai langkah sementara selagi menemukan penyebab sebenarnya, jangan pernah menutupinya dengan "retry sampai hijau" yang menyeluruh, itu hanya menyembunyikan bug.

15

Fixture & Factory

Tes butuh data dan state awal yang dikenal, dan cara Anda membangunnya menentukan apakah suite tetap terbaca atau membusuk jadi setup soup. Dua tool terkait: fixture mengelola setup dan teardown (dunia tempat tes berjalan, koneksi DB, dir temp, klien yang login), dan factory membangun objek tes dengan default masuk akal supaya tiap tes hanya menyatakan field yang benar-benar ia pedulikan.

// Go favors explicit helpers over magic. t.Helper() keeps failure line numbers useful.
// A "factory" with functional options: specify only the fields that matter to the test.
func newUser(t *testing.T, opts ...func(*User)) User {
    t.Helper()
    u := User{ID: "u1", Email: "default@x.com", Active: true} // sensible defaults
    for _, o := range opts { o(&u) }
    return u
}
func withEmail(e string) func(*User) { return func(u *User) { u.Email = e } }

// A "fixture" via t.Cleanup: setup returns the thing + auto-teardown.
func newTempStore(t *testing.T) *Store {
    t.Helper()
    dir := t.TempDir()                 // auto-removed after the test
    s := OpenStore(dir)
    t.Cleanup(func() { s.Close() })    // teardown runs even if the test fails
    return s
}

func TestSignup(t *testing.T) {
    store := newTempStore(t)
    u := newUser(t, withEmail("ada@x.com")) // only the relevant field is stated
    _ = store.Save(u)
    // ...assert...
}
import pytest

# pytest FIXTURES: setup before `yield`, teardown after. Injected by parameter name.
@pytest.fixture
def temp_store(tmp_path):           # tmp_path is a built-in fixture (auto-cleaned dir)
    store = Store(tmp_path)
    yield store                     # the test runs here
    store.close()                   # teardown, runs even if the test fails

# A FACTORY: defaults + overrides, so tests state only what they care about.
def make_user(**overrides):
    return User(**{"id": "u1", "email": "default@x.com", "active": True, **overrides})

def test_signup(temp_store):
    user = make_user(email="ada@x.com")   # only the relevant field is stated
    temp_store.save(user)
    # ...assert...

# Libraries like factory_boy / model_bakery scale this up for complex object graphs.
Keep the relevant data visible

Inti factory menyembunyikan setup yang tidak relevan, bukan menyembunyikan data yang diandalkan tes. Jika tes menegaskan sesuatu tentang user premium, kata "premium" harus muncul di tes itu (make_user(tier="premium")), bukan terkubur di fixture bersama tiga file jauhnya. Pembaca harus memahami premis tes tanpa menggali. Fixture bersama yang buram dan di-share berlebihan ("mystery guest") membuat kegagalan sulit didiagnosis dan tes sulit dipercaya.

Part V / Practice & Pipeline
16

TDD: Red / Green / Refactor

Test-Driven Development membalik urutan biasa: Anda menulis tes gagal dulu, lalu kode minimum untuk meloloskan, lalu membersihkan, mengulang dalam siklus kecil. Disiplinnya loop tiga ketukan yang terkenal: Red (tulis tes, lihat gagal), Green (buat lulus sesederhana mungkin), Refactor (perbaiki desain sementara tes tetap hijau). Menulis tes dulu memaksa Anda mendefinisikan perilaku yang diinginkan dan antarmuka bersih sebelum Anda terjerat implementasi.

The TDD cycle
Red -> Green -> Refactor, in small loops
REDfailing test first GREENsimplest pass REFACTORclean, stay green repeat, each loop adds one small, fully-tested behavior

Manfaat nyata TDD orde kedua: ia menghasilkan kode yang testable by construction (Anda tidak bisa menulis unit yang tak teruji jika tesnya datang dulu), ia menjaga Anda fokus pada satu perilaku kecil pada satu waktu, dan langkah "refactor" aman justru karena tes yang barusan Anda tulis menjaganya. Ia teknik desain sekaligus teknik pengujian.

A tool, not a religion

TDD bersinar saat kebutuhan jelas dan logika nontrivial (parser, aturan bisnis, algoritma). Ia canggung saat Anda menjelajah, spiking prototipe, membuat sketsa UI, menyelidiki API asing, tempat desainnya belum diketahui. Banyak engineer hebat menulis tes segera setelah kode alih-alih ketat sebelum. Yang penting perilakunya akhirnya teruji baik; test-first satu jalur efektif menuju itu, bukan satu-satunya yang valid.

17

Mocking Waktu, Keacakan & API Eksternal

Tiga sumber non-determinisme merusak tes jika dibiarkan nyata, jam, generator acak, dan jaringan, dan mereka tersangka biasa di balik flakiness (bagian 14). Obat untuk ketiganya sama: ubah dependensi tersembunyi menjadi yang disuntikkan (bagian 6) supaya tes mengendalikannya.

Waktu & keacakan: suntikkan keduanya

Kode yang memanggil time.Now() atau random() langsung tidak testable dan flaky, keluarannya berubah tiap run. Jadikan jam dan RNG dependensi yang diterima unit, lalu berikan jam tetap (selalu mengembalikan instan yang sama) atau RNG ber-seed di tes. Sekarang "token kedaluwarsa dalam 1 jam" atau "pilih shard acak" sepenuhnya deterministik dan bisa ditegaskan.

HTTP eksternal: stub batasnya

Jangan pernah memukul API pihak ketiga nyata dalam tes unit/integrasi, ia lambat, flaky, kena rate-limit, dan mungkin punya efek samping. Kedua ekosistem membiarkan Anda mencegat HTTP secara lokal: httptest.Server Go mendirikan server palsu yang Anda arahkan klien Anda; responses/respx Python menambal lapisan HTTP untuk mengembalikan respons siap. (Dan ingat bagian 5: prefer membungkus API di balik interface Anda sendiri supaya kebanyakan tes mock itu, menyisakan stub level HTTP untuk menguji adapter klien itu sendiri.)

// 1) Injected clock, deterministic time.
type Clock interface{ Now() time.Time }
type fixedClock struct{ t time.Time }
func (f fixedClock) Now() time.Time { return f.t }

func TestTokenExpiry(t *testing.T) {
    clk := fixedClock{t: time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)} // frozen
    tok := NewToken(clk, time.Hour)
    if tok.ExpiresAt != clk.Now().Add(time.Hour) {
        t.Error("expiry not computed from the injected clock")
    }
}

// 2) Stub an external HTTP API with httptest.Server, no real network.
func TestFetchRate(t *testing.T) {
    srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
        w.Write([]byte(`{"usd_inr": 83.2}`)) // canned response
    }))
    defer srv.Close()

    client := NewRatesClient(srv.URL) // point the client at the fake server's URL
    rate, _ := client.USDINR()
    if rate != 83.2 { t.Errorf("got %v", rate) }
}
import responses
from datetime import datetime, timezone

# 1) Injected clock, deterministic time (freezegun is a popular alternative).
def test_token_expiry():
    frozen = datetime(2026, 1, 1, 12, 0, tzinfo=timezone.utc)
    tok = make_token(now=lambda: frozen, ttl=3600)  # clock injected as a callable
    assert tok.expires_at == frozen.timestamp() + 3600

# 2) Stub external HTTP with `responses`, patches the HTTP layer, no real network.
@responses.activate
def test_fetch_rate():
    responses.add(
        responses.GET, "https://api.rates.com/usd-inr",
        json={"usd_inr": 83.2}, status=200,           # canned response
    )
    client = RatesClient("https://api.rates.com")
    assert client.usd_inr() == 83.2
Determinism is non-negotiable

Tes harus menghasilkan hasil sama di setiap run, di setiap mesin, urutan apa pun. Begitu waktu nyata, keacakan nyata, atau jaringan nyata menyusup, Anda menanam kegagalan flaky masa depan. Suntikkan dan kendalikan ketiganya, ini seam DI yang sama dari bagian 6 diterapkan ke tiga hal yang berubah sendiri.

18

Performance & Load Testing

Tes fungsional membuktikan kode benar; tes performa membuktikan ia cukup cepat, sumbu yang sepenuhnya berbeda, dan yang sangat penting bagi kekhawatiran scaling bab 18-19. Dua rasa berbeda: benchmark (mikro: seberapa cepat fungsi/endpoint ini?) dan load test (makro: bagaimana seluruh sistem berperilaku di bawah N pengguna serentak?).

JenisPertanyaanTool
BenchmarkSeberapa cepat / berapa alokasi untuk jalur kode ini?Go testing.B; Python pytest-benchmark
Load testThroughput & latensi di bawah lalu lintas serentak berkelanjutan?k6, Locust, Gatling, vegeta
Stress testDi mana ia pecah, dan bagaimana ia gagal?tool sama, dinaikkan melewati kapasitas
Soak testApakah ia menurun selama berjam-jam (bocor, fragmentasi)?tool sama, durasi panjang
// Benchmarks live beside tests; the tool tunes b.N until timing is stable.
func BenchmarkParseEvent(b *testing.B) {
    payload := []byte(`{"type":"click","ts":1717000000}`)
    b.ReportAllocs()                 // also report allocations/op
    for i := 0; i < b.N; i++ {       // the loop the framework times
        _, _ = ParseEvent(payload)
    }
}

// Run:  go test -bench=. -benchmem
//   BenchmarkParseEvent-8   3142051   382 ns/op   96 B/op   2 allocs/op
// Compare runs with `benchstat` to catch performance REGRESSIONS over time.
# pip install pytest-benchmark, the `benchmark` fixture times the callable.
def test_parse_event_perf(benchmark):
    payload = b'{"type":"click","ts":1717000000}'
    result = benchmark(parse_event, payload)   # runs it many times, reports stats
    assert result.type == "click"              # still assert correctness

# Output reports min/mean/median/stddev; --benchmark-compare flags regressions.
load_test.js / a k6 load test (language-agnostic, hits the running service)
import http from 'k6/http';
import { check } from 'k6';

export const options = {
  stages: [
    { duration: '30s', target: 100 },  // ramp up to 100 virtual users
    { duration: '1m',  target: 100 },  // hold
    { duration: '30s', target: 0 },    // ramp down
  ],
  thresholds: {
    http_req_duration: ['p(95)<300'],  // 95th-percentile latency must stay < 300ms
    http_req_failed:   ['rate<0.01'],  // error rate must stay under 1%
  },
};
export default function () {
  const res = http.get('https://staging.example.com/api/v1/health');
  check(res, { 'status 200': (r) => r.status === 200 });
}
# Run:  k6 run load_test.js, fails the build if a threshold is breached.
Measure percentiles, against a target, not your laptop

Rata-rata bohong, mean 50ms bisa menyembunyikan p99 2 detik yang merusak pengguna nyata. Selalu lacak latensi ekor (p95/p99). Dan jalankan load test terhadap lingkungan mirip-produksi, jangan pernah mesin dev Anda, dan nilai hasilnya terhadap SLO eksplisit (ambangnya), bukan getaran. Benchmark atau load test tanpa angka target cuma trivia.

19

Tes di CI/CD

Suite tes hanya bernilai jika ia berjalan otomatis, di setiap perubahan, sebagai gerbang. Di sinilah testing bertemu pipeline bab 21 (bagian 19 di sana): di setiap push, CI menjalankan tes, dan kegagalan memblokir merge atau deploy. Inti gradien kecepatan piramida (bagian 2) memberi umpan balik cepat di sini, jalankan tes unit milidetik di setiap commit, tes integrasi/E2E yang lebih berat di tahap yang tepat.

Tests as gates
Each layer runs at the stage where its cost is justified
every pushlint + unit (fast) on merge / PRintegration + DB pre-deployE2E (critical paths) post-deploysmoke (sec 12) Any red gate stops the line, the failure is caught before it reaches users.
.github/workflows/test.yaml / the test gate
name: tests
on: [push, pull_request]
jobs:
  unit:                          # fast, runs on every push
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: go test -race -cover ./...          # Go: race detector + coverage
      # - run: pytest -m "not integration" --cov  # Python: skip slow tests here

  integration:                   # heavier, real DB via services / testcontainers
    runs-on: ubuntu-latest
    needs: unit                  # only if unit tests passed
    services:
      postgres:
        image: postgres:16
        env: { POSTGRES_PASSWORD: p }
        ports: ["5432:5432"]
    steps:
      - uses: actions/checkout@v4
      - run: go test -tags=integration ./...     # or: pytest -m integration
# A failed job fails the check -> branch protection blocks the merge.
Make the suite fast & parallel

Jika tes lambat, orang melewatkannya, jadi kecepatan fitur. Jalankan tes paralel (Go melakukannya lintas paket secara default dan di dalam tes via t.Parallel(); pytest via pytest-xdist), pisahkan gerbang unit cepat dari tahap integrasi/E2E lambat, dan cache dependensi. Suite yang kembali dalam beberapa menit dijalankan; yang makan tiga puluh dilewati, dan tes yang tidak dijalankan tidak melindungi apa pun.

20

Strategi Pengujian

Tes punya biaya, untuk ditulis, dijalankan, dan dirawat saat kode berubah, jadi tujuannya tidak pernah "uji semuanya" melainkan kepercayaan maksimum per unit usaha. Strategi koheren kebanyakan set keputusan penilaian tentang apa yang diuji, di lapisan mana, dan apa yang dibiarkan.

Apa yang diuji berat

  • Logika & aturan bisnis: harga, validasi, state machine, apa pun dengan cabang dan kasus tepi. Tes unit murah cepat; pembayaran tertinggi (bagian 4, bagian 7).
  • Bug yang pernah Anda perbaiki: setiap perbaikan dapat tes regresi supaya tidak kembali (bagian 1).
  • Jalur kritis & uang: auth, pembayaran, integritas data dapat coverage di banyak lapisan, termasuk E2E tipis (bagian 12).
  • Seam integrasi: query Anda, serialisasi, dan kontrak antar layanan (bagian 8-11), tempat bug halus bersembunyi.

Apa yang diuji ringan atau tidak sama sekali

  • Kode sepele: getter polos, lem framework, kode hasil generasi. Mengujinya usaha tanpa wawasan.
  • Library pihak ketiga: asumsikan bekerja; uji pemakaian Anda terhadapnya, bukan internalnya (bagian 5).
  • Detail implementasi: helper privat dan urutan panggilan persis. Uji perilaku teramati supaya refactor tidak memicu kegagalan palsu.
The golden rule: test behavior, not implementation

Prinsip tunggal yang memisahkan suite yang membantu dari yang menyakiti: tegaskan apa yang dilakukan kode (input->output teramati dan efek sampingnya), jangan pernah bagaimana ia melakukannya secara internal. Tes fokus-perilaku selamat dari refactoring, mereka hijau selama kontrak berlaku, persis kepercayaan-untuk-berubah yang membenarkan tes sejak awal (bagian 1). Tes terkopel-implementasi pecah di setiap restrukturisasi, menghukum perbaikan, dan perlahan dihapus. Tulis tes yang akan disyukuri diri masa depan Anda, bukan dikutuk.

21

Crib-Sheet

Seluruh manual dipadatkan menjadi yang Anda raih saat tertekan.

KonsepSatu baris
Kenapa tesKepercayaan untuk berubah, spesifikasi yang bisa dieksekusi, jaring regresi, umpan balik cepat.
PiramidaBanyak unit (cepat) / beberapa integrasi / sedikit E2E (lambat). Jangan membaliknya.
AAAArrange, Act, Assert, satu perilaku per tes, nama deskriptif.
Tes unitSatu unit terisolasi, tanpa I/O, milidetik, presisi.
Test doubleDummy / Stub / Spy / Mock / Fake. Prefer state di atas verifikasi interaksi.
Jangan mockYang tidak Anda miliki, bungkus di balik interface Anda sendiri dan mock itu.
DISuntikkan kolaborator -> seam tempat double menyelip. Tak teruji = bau desain.
Table/parametrizeSatu body tes, banyak kasus bernama, coverage kasus tepi padat.
IntegrasiDB/deps nyata dirangkai, menangkap bug SQL, mapping, serialisasi.
DB nyataTestcontainers: engine sungguhan, sekali pakai, terisolasi. Bukan SQLite-untuk-Postgres.
Tes HTTPhttptest / TestClient, gerakkan handler dalam proses, tanpa socket.
Tes kontrakVerifikasi provider/consumer setuju pada antarmuka, tanpa menjalankan keduanya (Pact, buf).
E2ESeluruh sistem lewat antarmuka nyata, hanya perjalanan kritis; smoke-test setelah deploy.
CoverageMenemukan kode tak teruji; bukan ukuran kualitas. Prefer branch; jangan pernah target (Goodhart).
FlakyNon-determinisme: waktu, urutan, state bersama, konkurensi, jaringan. Obat = determinisme.
Fixture/factoryKelola setup/teardown & bangun data dengan default; jaga data relevan terlihat.
TDDRed -> Green -> Refactor. Tool desain; pakai di tempatnya, bukan dogmatis.
DeterminismeSuntikkan jam, seed RNG, stub jaringan, kendalikan ketiganya.
PerfBenchmark (mikro) + load test (makro). Lacak p95/p99 terhadap SLO, bukan rata-rata.
Gerbang CITes berjalan di setiap push dan memblokir merge/deploy. Jaga cepat & paralel.
StrategiKepercayaan per usaha. Uji logika, bug, uang, seam; lewati sepele & internal.
Aturan emasUji perilaku, bukan implementasi: selamat dari refactoring.

Seluruh topik dalam satu tarikan napas: tes otomatis ada untuk memberi kepercayaan mengubah kode (bagian 1), dan bentuk strategisnya piramida, banyak tes unit cepat, lebih sedikit integrasi, lapisan E2E tipis (bagian 2). Setiap tes Arrange-Act-Assert pada satu perilaku (bagian 3). Tes unit mengisolasi unit (bagian 4) memakai test double (bagian 5) yang dimungkinkan dependency injection (bagian 6), dengan kasus table-driven dan terparametri untuk coverage padat (bagian 7). Tes integrasi merangkai dependensi nyata (bagian 8), database nyata via testcontainers (bagian 9), handler via klien dalam proses (bagian 10), dan kesepakatan antar-layanan via tes kontrak (bagian 11). E2E hanya mencakup perjalanan kritis (bagian 12); coverage senter bukan tujuan (bagian 13); flakiness non-determinisme yang harus dihilangkan (bagian 14); dan fixture serta factory menjaga setup bersih (bagian 15). TDD menggerakkan desain dalam loop red/green/refactor (bagian 16), determinisme datang dari menyuntikkan waktu, keacakan, dan jaringan (bagian 17), performa sumbu sendiri dari benchmark dan load test terhadap SLO (bagian 18), dan suite hanya membayar upah dengan berjalan sebagai gerbang cepat di CI/CD (bagian 19). Yang terpenting: uji perilaku, bukan implementasi (bagian 20).

Berpijak pada dokumentasi Go testing & pytest / Testcontainers / literatur testing-pyramid (Cohn, Fowler) / contoh Go 1.22+ / Python 3.11+.