Menguji kode Haskell dengan HUnit untuk unit test, Hspec untuk spesifikasi gaya BDD, dan QuickCheck untuk property-based testing dengan forAll, menghitung coverage lewat hpc, serta strategi debugging memakai GHCi :t dan :i, Debug.Trace, dan :set +s untuk mengukur performa evaluasi.

Setelah di episode 9 kalian membangun proyek cabal yang rapi, episode ini tentang satu hal yang membedakan "kode jalan" dari "kode benar": testing. Karena fungsi Haskell pure, testing di sini adalah pengalaman yang luar biasa menyenangkan — tanpa setup, tanpa mock, tanpa database palsu. Uji sebuah fungsi pure hanyalah "panggil fungsi, cek hasilnya".
Mengapa bab ini penting? Karena kekuatan sesungguhnya Haskell muncul justru di sini: kombinasi pure function + property-based testing memungkinkan kalian menemukan bug yang tidak akan pernah ditemukan oleh contoh-contoh tangan. Kita juga akan menyiapkan perangkat debugging yang akan menemani kalian di semua episode berikutnya.
HUnit adalah framework unit test ala JUnit untuk Haskell. Test dinyatakan sebagai assertion:
import Test.HUnit
import MathOps (double)
tests :: Test
tests = TestList
[ "double 2 adalah 4" ~: double 2 ~?= 4
, "double 0 adalah 0" ~: double 0 ~?= 0
]
main :: IO ()
main = runTestTT tests >>= print~: menamai test, ~?= memeriksa kesetaraan. Jalankan dengan:
cabal run MathOpsTestHUnit sederhana dan eksplisit, tapi untuk test suite yang besar, Hspec menawarkan keterbacaan yang lebih baik.
Hspec menulis test sebagai spesifikasi yang terbaca seperti kalimat — gaya BDD (Behavior-Driven Development). Contohnya:
import Test.Hspec
import MathOps (double)
main :: IO ()
main = hspec $ do
describe "double" $ do
it "menggandakan angka positif" $ do
double 2 `shouldBe` 4
it "menggandakan nol menjadi nol" $ do
double 0 `shouldBe` 0Perhatikan describe (grup), it (satu contoh perilaku), dan shouldBe (assertion). Output Hspec adalah daftar spesifikasi hijau/merah yang sangat informatif. Hspec adalah pilihan default untuk kebanyakan proyek Haskell modern.
Inilah senjata rahasia Haskell. Alih-alih menulis contoh per contoh, kalian menulis properti yang harus selalu benar, dan QuickCheck membangkitkan ribuan input acak untuk mengujinya:
import Test.QuickCheck
-- properti: double (double x) selalu 4x
prop_double :: Int -> Bool
prop_double x = double (double x) == 4 * x
main :: IO ()
main = quickCheck prop_doubleQuickCheck memanggil prop_double dengan ratusan Int acak. Jika ada satu pun yang gagal, ia melaporkan counterexample:
+++ OK, passed 100 tests.Kekuatan sebenarnya: properti menangkap kontrak fungsi, bukan satu contoh. Jika kalian punya properti seperti "parse lalu print kembali menghasilkan nilai asal", test otomatis menelusuri ruang input yang sangat luas.
forAll membatasi generator untuk properti tertentu:
prop_rev :: [Int] -> Property
prop_rev xs = forAll (choose (0, 9)) $ \_ ->
reverse (reverse xs) == xsUntuk properti yang butuh input "berbentuk" (misalnya non-negatif), forAll dan generator custom sangat berguna — kita bedah lengkap di episode 19.
Tip
Pola yang paling produktif: Hspec untuk contoh yang mendokumentasikan perilaku, QuickCheck untuk kontrak yang melindungi refactor. Duo ini biasanya dipasang bersama — Hspec menjalankan QuickCheck di dalam test suite-nya lewat prop.
hpc (Haskell Program Coverage) mengukur baris mana yang sudah dieksekusi test:
cabal test --enable-coverageSetelah test selesai, hpc menghasilkan laporan HTML di dist-newstyle/ yang menandai kode ter-covvered (hijau), belum (merah), dan mixes. Target sehat untuk kode penting: 80-100%. Episode 19 akan membahas coverage sebagai bagian dari workflow CI.
GHCi adalah debugger yang luar biasa meski tampak sederhana. Tiga perintah inti:
ghci> :t map
map :: (a -> b) -> [a] -> [b]
ghci> :t fmap
fmap :: Functor f => (a -> b) -> f a -> f bKetika error tipe membingungkan, mulailah dengan :t pada ekspresi yang bermasalah — sering kali kesalahan terlihat jelas begitu tipe aktual muncul.
ghci> :i Monad
type Monad :: (* -> *) -> Constraint
class Applicative m => Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a
...:i menampilkan definisi lengkap: method, instance yang ada, dan superclass. Ini cara terbaik memahami apa yang bisa dipakai tipe tertentu.
ghci> :set +s
ghci> sum [1..1000000]
500000500000
it :: Int
(0.02 secs, 1,234,567 bytes):set +s menampilkan waktu dan alokasi memori tiap ekspresi — alat cepat untuk merasakan kompleksitas sebelum masuk ke profiler di episode 21.
Karena fungsi pure tidak boleh mencetak, kalian butuh backdoor sementara: Debug.Trace.
import Debug.Trace
doubleDebug :: Int -> Int
doubleDebug x = trace ("memproses " ++ show x) (x * 2)trace mencetak pesan ke stderr saat thunk dievaluasi, lalu mengembalikan nilai normal. Perhatikan kata "saat dievaluasi": dengan laziness, trace bisa muncul dalam urutan yang mengejutkan atau tidak muncul sama sekali. Ini alat untuk debug sementara — jangan sampai tertinggal di kode produksi.
Warning
Debug.Trace mengalahkan purity: ia menyelipkan efek samping ke fungsi yang seharusnya murni. Gunakan hanya untuk sesi debugging, dan bersihkan sebelum commit. Untuk pemahaman evaluasi yang lebih dalam, episode 21 akan memakai heap profiling sebagai gantinya.
Saat menemukan bug, urutan langkah yang efisien:
:t untuk memeriksa tipe ekspresi yang mencurigakan.:i untuk melihat apakah instance/fungsi yang dibutuhkan tersedia.trace sementara untuk melihat alur evaluasi.Inti yang harus dibawa pulang:
forAll mengendalikan generator.cabal test --enable-coverage.:t (tipe), :i (info), :set +s (timing/alokasi).Debug.Trace adalah backdoor sementara untuk melihat evaluasi; jangan dibawa ke produksi.Di episode 11 selanjutnya kita akan membahas monads: IO, Maybe, Either, State — memahami main :: IO () dan interaksi dunia luar, >>= dan do notation, monad Maybe untuk null-safety, Either untuk error yang terstruktur, dan Reader/Writer/State untuk konfigurasi dan state lokal. Sampai jumpa di episode 11!