M
Mr Sugiarto
Developer
28 Sep 2026 7 min read

Lima bab terakhir kita menulis empat jenis test yang berbeda untuk Order Service — masing-masing dengan trade-off kecepatan dan cakupan yang berbeda. Bab penutup Fase 3 ini kita satukan semuanya jadi satu gambaran besar yang disebut test pyramid, lalu pastikan seluruh test itu benar-benar dijalankan otomatis, bukan cuma mengandalkan disiplin pribadi tiap developer untuk menjalankannya manual sebelum push.

Konsep Test Pyramid

Test pyramid adalah cara berpikir tentang komposisi test suite yang sehat: banyak test cepat dan murah di lapisan dasar, makin sedikit test di lapisan yang makin lambat dan mahal di atasnya. Bukan aturan kaku soal jumlah persis, tapi prinsip: kalau sebuah bug bisa terdeteksi oleh test yang cepat, jangan tunggu sampai ketahuan oleh test yang lambat — dan kalau test lambat memang satu-satunya cara membuktikan sesuatu (misalnya "apakah data benar-benar tersimpan di database"), jaga jumlahnya tetap kecil dan fokus ke skenario paling kritis saja.

Order Service yang kita bangun sepanjang Fase 3 ini kebetulan memetakan pyramid ini dengan rapi, dari dasar ke puncak:

Test class Kecepatan Butuh apa untuk jalan Apa yang dibuktikan
OrderServiceTest (bab 19) Sangat cepat (milidetik) Tidak ada — OrderRepository di-mock Business logic OrderService benar secara isolasi: perhitungan totalPrice, aturan status, validasi quantity
OrderServiceThresholdParameterizedTest (bab 20) Sangat cepat (milidetik) Tidak ada — sama seperti di atas Titik batas business rule REVIEW_THRESHOLD benar persis di, dan di sekitar, ambangnya
OrderControllerTest (bab 23) Cepat (Spring context sebagian, tanpa DB) @WebMvcTest boot layer web saja Kontrak HTTP: status code, bentuk JSON response, penanganan validasi & error — OrderService di-mock
OrderContextLoadTest (bab 21) Lambat (Spring context penuh + container DB) Docker + Testcontainers PostgreSQL Seluruh application context bisa nyala dan bean-bean utama ter-wiring dengan benar
OrderIntegrationTest (bab 22) Paling lambat (Spring context penuh + HTTP + DB) Docker + Testcontainers PostgreSQL Alur end-to-end sungguhan: HTTP masuk, tersimpan ke Postgres, terbaca kembali dengan data yang sama

Yang paling di dasar (OrderServiceTest, OrderServiceThresholdParameterizedTest) tidak menyentuh Spring maupun database sama sekali — di sinilah seharusnya sebagian besar test kamu berada, karena murah untuk ditulis, murah dijalankan berkali-kali, dan gagalnya langsung menunjuk ke baris logic yang salah. OrderControllerTest ada di tengah — dia menyentuh lapisan HTTP sungguhan tapi database-nya di-mock, jadi tetap relatif murah. Yang di puncak (OrderContextLoadTest, OrderIntegrationTest) paling mahal karena butuh Docker dan container database sungguhan menyala — kita sengaja menjaga jumlahnya tetap sedikit (satu smoke test, dua skenario end-to-end kritis), bukan mencoba menutup setiap kombinasi skenario lewat lapisan ini.

Kalau kamu terbalik — menulis banyak @SpringBootTest untuk hal-hal yang sebenarnya bisa diuji lewat unit test biasa — test suite-mu akan lambat dijalankan, dan makin lambat test suite, makin besar godaan untuk melewatkannya sama sekali sebelum push. Test pyramid pada dasarnya adalah strategi supaya test suite tetap layak dijalankan setiap saat.

Kenapa CI Gate Diperlukan, Bukan Cuma Disiplin Pribadi

Menulis test bagus itu satu hal; memastikan test itu benar-benar dijalankan setiap kali ada perubahan kode adalah hal lain. Kalau cuma mengandalkan tiap developer mengingat untuk menjalankan ./gradlew test sebelum push, cepat atau lambat akan ada yang lupa — apalagi kalau test integration-nya butuh Docker jalan dulu di mesin lokal. CI (Continuous Integration) menutup celah ini: setiap push dan pull request otomatis memicu seluruh test suite dijalankan di server, terlepas dari apakah developer-nya ingat menjalankannya secara lokal atau tidak. Kalau ada test yang merah, itu ketahuan di PR sebelum kode sempat masuk ke branch utama — bukan ketahuan belakangan setelah menyatu dengan perubahan orang lain.

Membaca ci.yml

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: '21'

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      - name: Run tests
        run: ./gradlew test --no-daemon

      - name: Upload test report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-report
          path: build/reports/tests/test

Mari bedah baris demi baris.

on: push dan pull_request, keduanya branches: [main] — workflow ini terpicu dua kali di titik yang berbeda dalam siklus hidup sebuah perubahan kode: setiap kali ada push langsung ke main, dan setiap kali ada pull request yang menyasar main (baik dibuka baru maupun ditambah commit baru). Yang kedua ini yang paling penting dalam praktik: artinya setiap PR akan menjalankan test suite lengkap sebelum kode itu di-merge, bukan sesudahnya.

runs-on: ubuntu-latest — job ini jalan di mesin virtual Ubuntu yang disediakan GitHub. Ini penting untuk Testcontainers: ubuntu-latest sudah terpasang Docker secara default, jadi test seperti OrderIntegrationTest dan OrderContextLoadTest yang butuh Testcontainers bisa langsung jalan di CI tanpa langkah instalasi Docker tambahan apa pun. Kalau kamu memilih runner lain yang tidak punya Docker preinstalled, kamu harus menambahkan step instalasi Docker secara manual sebelum test bisa jalan — salah satu alasan ubuntu-latest jadi pilihan default yang praktis untuk project yang memakai Testcontainers.

actions/checkout@v4 — meng-clone kode repo ke dalam runner, langkah standar di hampir semua workflow GitHub Actions; tanpa ini runner-nya kosong, tidak ada kode apa pun untuk diuji.

actions/setup-java@v4 dengan distribution: temurin dan java-version: '21' — memasang JDK 21 (Eclipse Temurin, distribusi OpenJDK yang stabil dan gratis dipakai) di runner. Ingat dari bab pengantar seri ini: JDK 21 adalah syarat minimal untuk Spring Boot versi yang dipakai Order Service, jadi versi ini harus dipasang eksplisit dan cocok dengan yang dipakai untuk development lokal — kalau versinya beda, ada risiko kode yang jalan di lokal ternyata gagal compile atau berperilaku beda di CI.

chmod +x gradlew — memberi izin eksekusi ke script gradlew (Gradle Wrapper). Script ini biasanya sudah punya izin eksekusi saat di-commit dari mesin Unix/macOS, tapi git tidak selalu mempertahankan bit permission ini dengan konsisten lintas platform, jadi step ini jadi jaring pengaman supaya ./gradlew test di langkah berikutnya tidak gagal cuma karena "permission denied".

./gradlew test --no-daemon — inti dari seluruh workflow ini: menjalankan seluruh test suite, dari OrderServiceTest yang milidetik sampai OrderIntegrationTest yang butuh Testcontainers. Flag --no-daemon mematikan Gradle daemon (proses background yang biasanya dipertahankan Gradle untuk mempercepat build berikutnya di mesin yang sama) — di CI, tiap run terjadi di runner yang baru dan sekali pakai, jadi tidak ada gunanya mempertahankan daemon yang tidak akan dipakai lagi setelah job ini selesai; mematikannya menghindari proses menggantung yang bisa memperlambat atau mengganggu job.

upload-artifact@v4 dengan if: always() — mengunggah laporan test (build/reports/tests/test, laporan HTML standar yang dihasilkan Gradle) sebagai artifact yang bisa diunduh dari halaman workflow run di GitHub. Bagian if: always() krusial: tanpa ini, step upload cuma jalan kalau step sebelumnya (Run tests) sukses — padahal justru saat test gagal itulah laporan detailnya paling dibutuhkan, untuk melihat persis test mana yang merah dan kenapa. if: always() memastikan laporan tetap ter-upload baik test-nya hijau maupun merah.

Kenapa Menjalankan SELURUH Test Suite di CI, Termasuk yang Testcontainers, Adalah Kuncinya

Kamu bisa saja membuat CI cuma menjalankan test yang cepat (unit test dan controller test) dan melewatkan yang butuh Testcontainers, dengan alasan "biar cepat". Tapi kalau begitu, OrderIntegrationTest — satu-satunya test yang membuktikan alur HTTP-ke-database benar-benar bekerja, dan yang di bab 22 kemarin justru menangkap bug scale BigDecimal yang tidak akan pernah ketahuan dari test lain — jadi cuma dijalankan kalau developer-nya ingat menjalankannya manual secara lokal. Di situ test pyramid berhenti jadi jaminan tim dan kembali jadi soal disiplin pribadi.

Menjalankan ./gradlew test secara utuh di CI — termasuk lapisan Testcontainers yang paling lambat — adalah yang membuat test pyramid ini benar-benar berfungsi sebagai gate: kode yang menyalahi kontrak apa pun, di lapisan mana pun, tidak akan bisa merge ke main, terlepas dari siapa yang menulisnya atau apakah dia sempat menjalankan ./gradlew test secara lokal. Karena ubuntu-latest sudah punya Docker terpasang, ongkos menjalankan seluruh pyramid ini di CI tidak butuh setup tambahan apa pun — satu-satunya "harga" yang dibayar adalah waktu eksekusi yang sedikit lebih lama dibanding kalau cuma menjalankan unit test saja, harga yang sepadan dengan jaminan yang didapat.

Menutup Fase 3

Dengan CI gate ini, Order Service sekarang punya empat lapisan test yang saling melengkapi dan dijalankan otomatis di setiap perubahan kode — dari logic murni sampai alur HTTP-ke-database sungguhan, tidak ada satu pun yang bergantung pada ingatan seseorang untuk menjalankannya. Ini menutup Fase 3.

Fase 4 berikutnya: kita keluar dari satu service dan mulai bicara soal microservice architecture sungguhan — kapan pola ini masuk akal, dan bagaimana Order Service berkomunikasi dengan service lain.

M
Mr Sugiarto

Developer

Bagian dari Series: Kotlin Backend Microservice - Belajar Fundamental sampai Microservice Nyata

Belajar Kotlin + Spring Boot dari fundamental bahasa (null safety, OOP, coroutines) sampai membangun sistem microservice sungguhan - REST API, Postgre...

Lihat Series Lengkap