Pengujian properti menyapu ruang masukan yang luas, tetapi sebagian kebenaran paling baik dinyatakan sebagai kasus tertentu yang nilainya sudah diketahui, misalnya hasil tepat dari satu perhitungan atau kasus tepi yang ditemukan dari laporan bug. Unit test menampung kasus-kasus itu, dan pada ekosistem Haskell pasangan yang lazim berupa framework tasty beserta pustaka asersi HUnit. Topik ini membahas pembentukan suite, pengelompokan kasus, pembacaan kegagalan, serta pembagian kerja antara unit test dan properti.
Pasangan pustaka ini dipasang sekali melalui Cabal, dan paket tasty-hunit sekaligus membawa HUnit sebagai dependensinya. Setelah pemasangan selesai, modul Test.Tasty dan Test.Tasty.HUnit dapat diimpor dari program uji mana pun.
# tidak-dijalankan
# pasang tasty beserta integrasi HUnit ke lingkungan paket pengguna
cabal install --lib tasty tasty-hunitKasus tunggal#
Setiap kasus dinyatakan sebagai testCase yang membawa nama serta satu pernyataan HUnit, dan operator utamanya adalah @?= yang membandingkan nilai aktual dengan nilai yang diharapkan. Kumpulan kasus diserahkan kepada defaultMain untuk dijalankan.
-- Suite pertama dengan tasty dan HUnit
import Test.Tasty
import Test.Tasty.HUnit
main :: IO ()
main = defaultMain $ testGroup "Uji satu kasus"
[ testCase "dua ditambah tiga" (2 + 3 @?= 5)
, testCase "membalik tiga karakter" (reverse "abc" @?= "cba")
]Uji satu kasus
dua ditambah tiga: OK
membalik tiga karakter: OK
All 2 tests passed (0.00s)Keluaran menampilkan struktur suite sebagai pohon, yaitu nama kelompok pada baris pertama dan setiap kasus menjorok di bawahnya. Baris penutup merangkum jumlah kasus yang lolos beserta lamanya. Nama pada testCase berfungsi sebagai keterangan bagi pembaca, sehingga kasus yang gagal terbaca langsung dari namanya tanpa membaca kode testnya. Persamaan pada @?= ditulis dengan nilai aktual di kiri dan nilai harapan di kanan, kebiasaan yang membuat pembacaan keluaran kegagalan tetap konsisten.
Suite berkelompok#
Suite sungguhan menguji fungsi dengan beberapa kasus sekaligus, dan kasus-kasus itu dikelompokkan menurut fungsi yang diuji. Fungsi testGroup bersarang membentuk pohon yang mendalam, satu kelompok untuk setiap fungsi dan satu kasus untuk setiap keadaan yang relevan.
-- Suite terkelompok untuk penghitung kata
import Test.Tasty
import Test.Tasty.HUnit
import Data.Char (toUpper)
banyakKata :: String -> Int
banyakKata = length . words
hurufAwal :: String -> String
hurufAwal = unwords . map kapitalisasi . words
where
kapitalisasi [] = []
kapitalisasi (c:cs) = toUpper c : cs
main :: IO ()
main = defaultMain $ testGroup "Penghitung kata"
[ testGroup "banyakKata"
[ testCase "teks normal" (banyakKata "satu dua tiga" @?= 3)
, testCase "spasi berlebih" (banyakKata " satu dua " @?= 2)
, testCase "teks kosong" (banyakKata "" @?= 0)
]
, testGroup "hurufAwal"
[ testCase "satu kata" (hurufAwal "satu" @?= "Satu")
, testCase "beberapa kata" (hurufAwal "satu dua" @?= "Satu Dua")
]
]Penghitung kata
banyakKata
teks normal: OK
spasi berlebih: OK
teks kosong: OK
hurufAwal
satu kata: OK
beberapa kata: OK
All 5 tests passed (0.00s)Struktur kelompok membuat laporan kegagalan tetap terbaca ketika jumlah kasus bertambah, karena kasus yang gagal ditemukan melalui jalur kelompoknya. Kasus seperti " satu dua " memiliki tempat yang wajar di sini, yaitu keadaan masukan yang khusus dan nilainya sudah diketahui. Suite ini juga dapat diperbesar tanpa mengubah bentuknya, misalnya menambah kelompok untuk fungsi ketiga pada daftar yang sama.
Penamaan kasus menuntut perhatian tersendiri, karena nama itulah yang tercetak pada laporan dan bukan isi asersinya. Nama yang menyebut masukan beserta harapannya, misalnya spasi berlebih, langsung menceritakan keadaan yang diuji, sedangkan nama generik seperti uji 1 menyembunyikan maksudnya dan memaksa pembaca laporan membuka berkas testnya. Kebiasaan yang lazim berupa menamai kasus dengan kalimat singkat yang menyebut keadaan masukannya.
Membaca kegagalan#
Fungsi rataRata berikut menghitung rata-rata menggunakan pembagian bulat, dan salah satu kasusnya menyentuh keadaan yang belum ditangani. Suite tetap dijalankan, dan tasty melaporkan kasus yang gagal tanpa menghentikan kasus lainnya.
-- Test yang gagal mengungkap kasus tepi yang terlewat
import Test.Tasty
import Test.Tasty.HUnit
rataRata :: [Int] -> Int
rataRata xs = sum xs `div` length xs
main :: IO ()
main = defaultMain $ testGroup "Statistik"
[ testCase "rata-rata dua nilai" (rataRata [4, 6] @?= 5)
, testCase "rata-rata list kosong" (rataRata [] @?= 0)
]Statistik
rata-rata dua nilai: OK
rata-rata list kosong: FAIL
Exception: divide by zero
Use -p '/rata-rata list kosong/' to rerun this test only.
1 out of 2 tests failed (0.00s)Kasus yang gagal dilaporkan dengan penyebabnya, yaitu exception pembagian dengan nol yang muncul karena length [] bernilai nol. Baris saran -p '/rata-rata list kosong/' menunjukkan pola untuk menjalankan ulang satu kasus saja, cara yang berguna ketika suite berisi ratusan kasus. Keluaran program ini juga memperlihatkan peran unit test sebagai dokumen, yaitu keputusan yang belum diambil tampak sebagai kegagalan yang tersimpan, bukan sebagai catatan yang mudah hilang. Keadaan list kosong pada rata-rata memang tidak memiliki jawaban pembagian bulat, sehingga perbaikannya berupa mengubah tipe hasil menjadi Maybe Int atau menolak masukan kosong di batas, keputusan yang sebaiknya dibuat secara sadar.
Unit test atau properti#
Keduanya melayani kebutuhan yang berbeda, dan tabel berikut merangkum pembagian kerjanya.
| Aspek | Unit test (tasty, HUnit) | Properti (QuickCheck) |
|---|---|---|
| Bentuk pernyataan | kasus tunggal dengan nilai pasti | sifat untuk segenap masukan |
| Kasus yang dicakup | yang ditulis secara eksplisit | yang dihasilkan acak |
| Contoh yang paling cocok | hasil tepat, kasus tepi dari bug | sifat pembatalan dan jangkauan |
| Biaya menulis | naik sebanding dengan jumlah kasus | satu properti mencakup banyak kasus |
Pembagian kerja yang lazim berupa menulis properti terlebih dahulu untuk sifat yang umum, kemudian menyimpan contoh pelanggaran hasil QuickCheck dan kasus tepi yang khusus sebagai unit test. Suite yang baik memuat keduanya, sebab kasus tertentu memberi kejelasan saat kegagalan, sedangkan properti memberi jangkauan yang tidak mungkin dicapai penulisan tangan.
Kesalahan umum#
Kesalahan pertama adalah menulis asersi yang tidak pernah bisa gagal, misalnya membandingkan hasil dengan dirinya sendiri. Kasus semacam itu selalu OK dan memberi rasa aman yang keliru, sehingga nilai harapan pada @?= harus benar-benar berasal dari luar kode testnya, yaitu angka atau teks yang ditulis tangan.
Kesalahan kedua berupa membiarkan suite gagal pada repository yang sedang dikerjakan. Perintah suite yang berakhir gagal menghasilkan kode keluar selain nol, sehingga alat integrasi berkelanjutan menandai build itu rusak, keadaan yang tepat agar perbaikan segera dilakukan. Kesalahan ketiga berupa menggantikan properti dengan unit test untuk sifat yang berlaku umum, misalnya menguji reverse pada tiga contoh saja, padahal prop_balikDuaKali pada topik QuickCheck menyatakan sifat yang sama untuk list apa pun.
Ringkasan#
- Suite tasty terdiri atas
testCasebernama yang berisi asersi HUnit, dijalankan melaluidefaultMain. testGroupbersarang membentuk pohon kasus yang membuat laporan kegagalan tetap terbaca.- Kegagalan dilaporkan beserta penyebabnya dan pola
-puntuk menjalankan ulang satu kasus. - Unit test menyimpan kasus pasti dan pelanggaran yang ditemukan QuickCheck, sedangkan properti menyapu ruang masukan yang luas.
Referensi#
- Test.Tasty — Hackage
— Dokumentasi inti tasty, termasuk
defaultMaindantestGroup. - Test.Tasty.HUnit — Hackage
— Dokumentasi integrasi HUnit pada tasty beserta operator
@?=dantestCase. - HUnit package — Hackage — Halaman paket HUnit beserta panduan asersinya.