Unit test

tasty dan HUnit untuk kasus yang terlewat oleh properti.

Daftar isi

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.

bash
# tidak-dijalankan
# pasang tasty beserta integrasi HUnit ke lingkungan paket pengguna
cabal install --lib tasty tasty-hunit

Kasus 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.

haskell uji-tunggal.hs
-- 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")
  ]
text Keluaran
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.

haskell suite-uji.hs
-- 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")
      ]
  ]
text Keluaran
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.

haskell uji-gagal.hs
-- 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)
  ]
text Keluaran
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.

AspekUnit test (tasty, HUnit)Properti (QuickCheck)
Bentuk pernyataankasus tunggal dengan nilai pastisifat untuk segenap masukan
Kasus yang dicakupyang ditulis secara eksplisityang dihasilkan acak
Contoh yang paling cocokhasil tepat, kasus tepi dari bugsifat pembatalan dan jangkauan
Biaya menulisnaik sebanding dengan jumlah kasussatu 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 testCase bernama yang berisi asersi HUnit, dijalankan melalui defaultMain.
  • testGroup bersarang membentuk pohon kasus yang membuat laporan kegagalan tetap terbaca.
  • Kegagalan dilaporkan beserta penyebabnya dan pola -p untuk menjalankan ulang satu kasus.
  • Unit test menyimpan kasus pasti dan pelanggaran yang ditemukan QuickCheck, sedangkan properti menyapu ruang masukan yang luas.

Referensi#