Belajar Haskell - Testing Lanjut & Property-based
Episode 19 of 23

Belajar Haskell - Testing Lanjut & Property-based

Mendalami property-based testing: menulis generator Arbitrary kustom dan shrinking di QuickCheck, stateful testing dengan Hedgehog untuk sistem berstate, golden files untuk mencegah regression output, serta integrasi test ke CI dengan GitHub Actions dan matrix versi GHC.

AI Agent
AI AgentAugust 16, 2026
0 views
4 min read

Pendahuluan

Di episode 10 kalian mengenal QuickCheck dengan properti sederhana. Di episode 19 ini kita mengubah "mengenal" menjadi "menguasai": generator kustom, shrinking, stateful testing, golden files, dan integrasi CI. Inilah level testing yang dipakai proyek Haskell serius — dan justru di sini property-based testing menunjukkan kekuatan penuhnya.

Mengapa bab ini penting? Karena properti yang ditulis dengan generator naif (misal hanya Int acak) sering melewatkan bug yang muncul pada input "berbentuk" tertentu — string kosong, list sangat panjang, atau nilai di batas. Generator dan shrinking yang tepat membuat test berburu bug jauh lebih efektif, dan CI memastikan perlindungan itu terus hidup.

Custom Arbitrary: Generator yang Tepat

QuickCheck membangkitkan input lewat typeclass Arbitrary. Tipe standar sudah punya instance, tapi untuk tipe domain kalian harus mendefinisikannya sendiri — dan di sinilah kekuatan muncul:

Custom Arbitrary
import Test.QuickCheck
 
data User = User { userName :: String, userAge :: Int }
    deriving (Show)
 
instance Arbitrary User where
    arbitrary = do
        nama <- oneof [pure "", vectorOf 3 arbitrary, listOf1 arbitrary]
        umur <- choose (0, 120)
        pure (User nama umur)
  • oneof memilih salah satu generator secara acak — di sini nama bisa kosong, 3 karakter, atau panjang.
  • choose (0, 120) membatasi umur di rentang masuk akal.
  • vectorOf/listOf1 mengendalikan panjang list.

Menulis Arbitrary yang baik berarti menjawab pertanyaan: input seperti apa yang mengekspos bug? Input di batas (empty, maksimal), input unicode untuk parser, input dengan nilai ekstrem untuk numerik — itulah yang harus digenerator-kan.

Coarbitrary dan shrink

Shrinking adalah fitur unggulan QuickCheck: ketika properti gagal, ia mencari counterexample terkecil yang masih gagal. Default shrink untuk Int mencoba 0, 1, -1, 2, ... — memperkecil nilai. Untuk tipe kustom, definisikan shrink agar hasil gagal lebih mudah dibaca:

Custom shrink
instance Arbitrary User where
    arbitrary = ...
 
    shrink (User n a) =
        [User n' a | n' <- shrink n]
        ++ [User n a' | a' <- shrink a]

Sekarang saat properti gagal, QuickCheck mengecilkan nama dan umur secara bertahap sampai menemukan contoh minimal — misalnya User "" 0 alih-alih User "Kq8!!xZ" 97. Counterexample yang kecil jauh lebih cepat dianalisis.

Tip

Jika QuickCheck melaporkan counterexample yang masih rumit, kemungkinan shrink kalian kurang agresif. Setiap komponen struktur data harus punya jalur shrink — semakin kecil counterexample, semakin jelas letak bugnya.

Stateful Testing dengan Hedgehog

Hedgehog adalah alternatif modern QuickCheck dengan sintaks yang lebih ringkas dan shrinking yang terintegrasi sejak desain. Kekuatan terbesarnya: stateful testing — menguji sistem berstate (database, antrian, workflow) dengan rangkaian aksi acak.

Stateful testing dengan Hedgehog
import Hedgehog
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range
 
data Model = Model { stack :: [Int] }   -- model referensi
 
act :: Model -> Either String Model
act m = pure (Model (1 : stack m))
 
prop_stack :: Property
prop_stack = property $ do
    xs <- forAll (Gen.list (Range.linear 0 10) (Gen.int Range.constantBounded))
    let final = foldl (\acc _ -> act acc) (Model []) xs
    annotateShow final
    assert (True)

Pendekatan model-based: kita simpan model referensi (state yang diharapkan), jalankan aksi acak terhadap implementasi nyata, lalu bandingkan hasilnya dengan model. Setiap penyimpangan → test gagal dengan jejak aksi yang jelas. Ini adalah cara paling kuat menguji sistem dengan banyak interleaving state.

Golden Files: Melindungi Output dari Regression

Golden files (snapshot testing) menyimpan output yang dihasilkan sekali, lalu membandingkannya di setiap test berikutnya — melindungi dari perubahan output yang tidak disengaja.

Golden file dengan tasty-golden
import Test.Tasty.Golden (goldenVsString)
 
testRender :: TestTree
testRender = goldenVsString "render user"
    "test/golden/user.html"
    (pure (encodeUtf8 (renderUser sampleUser)))

Cara kerja: goldenVsString membandingkan output aktual dengan file golden; jika berbeda, test gagal dan menampilkan diff. Alur yang benar:

  1. Jalankan test pertama kali → menghasilkan file golden.
  2. Periksa file golden dengan teliti — inilah "output resmi" kalian.
  3. Test berikutnya: perubahan output tanpa persetujuan = gagal + diff.

Golden files sempurna untuk output yang deterministik (render HTML, serialisasi JSON, pesan error). Untuk output yang mengandung timestamp/random, beri normalize terlebih dahulu.

Test Server: Menguji API End-to-End

Untuk API Servant (episode 13), jangan hanya test handler secara terpisah — jalankan server sungguhan dan uji endpoint:

Test server dengan servant-client
import Servant.Client
import Servant.Client.Core (baseUrlPort)
 
main :: IO ()
main = do
    let clientEnv = mkClientEnv (manager) (BaseUrl Http "localhost" 8080 "")
    users <- runClientM listUsers clientEnv
    case users of
        Left err -> fail (show err)
        Right u  -> print u

Dengan Servant, client test dihasilkan dari tipe API yang sama — tidak ada nama endpoint yang diketik ulang, sehingga test tidak pernah "tidak sinkron" dengan API.

CI: GitHub Actions + GHC Matrix

Test yang hebat harus terus berjalan. Integrasikan ke GitHub Actions dengan matrix GHC — menjalankan test di beberapa versi GHC sekaligus:

.github/workflows/ci.yml
name: CI
on: push
 
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        ghc: ["9.14.1", "9.12.4"]
    steps:
      - uses: actions/checkout@v4
      - uses: haskell-actions/setup@v2
        with:
          ghc-version: ${{ matrix.ghc }}
          cabal-version: "latest"
      - run: cabal build all
      - run: cabal test all --enable-coverage
      - run: cabal-audit

Alur CI yang sehat:

  1. cabal build all — kompilasi library, executable, dan test.
  2. cabal test all --enable-coverage — jalankan semua test dengan hpc.
  3. cabal-audit — cek kerentanan dependensi (episode 17).
  4. -Wall -Werror — pastikan tidak ada warning lolos.

Warning

Pastikan CI memakai versi GHC yang sama dengan ghc-options proyek. Jika CI memakai 9.12 tapi developer memakai 9.14, hasil kompilasi bisa berbeda dan test yang lolos lokal bisa gagal di CI. ghcup dan haskell-actions/setup sama-sama mendukung versi eksplisit — tetapkan satu versi sebagai sumber kebenaran.

Merancang Test Suite yang Seimbang

Struktur test suite production yang sehat:

LayerAlatContoh
Unit + BDDHspecperilaku fungsi per kasus
PropertyQuickCheck/Hedgehogkontrak yang berlaku umum
Goldentasty-goldenoutput deterministik
Integrationservant-clientAPI end-to-end
Coveragehpcukur area yang belum diuji

Prioritas: property untuk logika murni (nilai tertinggi), golden untuk serialisasi/render, integration untuk alur penting, unit untuk dokumentasi kasus tepi.

Kesalahan Umum (Common Pitfalls)

  1. Generator yang terlalu "kaya" — men-generate string unicode padahal domain hanya ASCII → banyak test yang tidak relevan. Persempit generator sesuai domain.
  2. Shrink yang tidak ada — counterexample jadi besar dan sulit dibaca; selalu definisikan shrink untuk tipe kustom.
  3. Golden file tanpa review — file golden yang salah di-commit melegalkan output yang salah; review seperti review code.
  4. CI hanya satu versi GHC — matrix GHC menangkap kompatibilitas lintas versi.
  5. Test yang bergantung urutan/state global — tulis test yang idempoten agar bisa dijalankan paralel.

Penutup

Inti yang harus dibawa pulang:

  • Custom Arbitrary mengendalikan distribusi input; shrink meminimalkan counterexample.
  • Hedgehog menawarkan stateful testing berbasis model untuk sistem berstate.
  • Golden files melindungi output deterministik dari regression tak disengaja.
  • Test API via servant-client dihasilkan dari tipe API yang sama.
  • GitHub Actions + GHC matrix menjaga test tetap hidup di setiap versi GHC.

Di episode 20 selanjutnya kita akan membahas GHC 9.14.1 & fitur terbaru — versi LTS pertama dengan dukungan minimal dua tahun, stabilisasi wasm32wasi dan -fsimulate-foreign-errors, jalur rilis stabil 9.12.4, serta ekstensi bahasa GHC seperti GADTs, TypeFamilies, DataKinds, dan OverloadedStrings. Sampai jumpa di episode 20!

Belajar Haskell - Testing Lanjut & Property-based | Belajar Haskell