Titik pemeriksaan adalah pengujian yang dijalankan berulang pada setiap langkah pengerjaan, bukan hanya sekali pada akhir proyek. Modul Statistik dari proyek ini sudah berfungsi penuh, tetapi berfungsi belum berarti benar, sehingga setiap kontraknya kini disandingkan dengan tiga alat yang melengkapi satu sama lain, yaitu properti QuickCheck, unit test tasty, dan tolok ukur criterion. Topik ini memandu pemasangan titik-titik pemeriksaan tersebut pada modul yang sama, dimulai dari properti pertama yang menemukan kasus tepi, dilanjutkan dengan perbaikannya, penguncian angka konkret, dan diakhiri dengan pengukuran yang menegaskan pilihan implementasi.
Properti sebagai titik pemeriksaan pertama#
Modul yang diuji ditampilkan kembali sebagai acuan, yaitu dua fungsi kecil yang kontraknya akan diperiksa oleh seluruh titik pemeriksaan pada topik ini.
-- tidak-dijalankan
-- Modul Statistik: kontrak dan implementasinya
module Statistik (rataRata, rentang) where
rataRata :: [Int] -> Int
rataRata xs = sum xs `div` length xs
rentang :: [Int] -> Int
rentang xs = maximum xs - minimum xsProperti menjadi titik pemeriksaan pertama karena biayanya paling kecil, yaitu fungsi pendek yang menyatakan sifat yang seharusnya selalu berlaku. Properti berikut melekat pada fungsi rentang, dan pernyataannya sederhana, yaitu rentang sekumpulan angka tidak pernah negatif. Properti sekecil ini dapat saja tampak sehat, sehingga jalankannya terlebih dahulu sebelum menilai apa pun.
-- Properti pertama yang belum menangani list kosong
import Test.QuickCheck
import Statistik
-- rentang tidak pernah negatif untuk list apa pun
prop_rentangTidakNegatif :: [Int] -> Bool
prop_rentangTidakNegatif xs = rentang xs >= 0
main :: IO ()
main = quickCheck prop_rentangTidakNegatif*** Failed! (after 1 test):
Exception:
Prelude.maximum: empty list
CallStack (from HasCallStack):
error, called at libraries/base/GHC/List.hs:1644:3 in base:GHC.List
errorEmptyList, called at libraries/base/GHC/List.hs:715:28 in base:GHC.List
maximum, called at libraries/base/Data/Foldable.hs:745:15 in base:Data.Foldable
[]Properti tersebut gagal pada kasus pertama, dan pelaporannya memuat dua informasi penting. Contoh pelanggarnya berupa list kosong, sedangkan penyebabnya berupa exception Prelude.maximum: empty list, karena rentang [] memanggil maximum dan minimum pada list yang tidak memiliki satu elemen pun. Kegagalan ini bukan kesalahan penghitungan, melainkan ketidaksesuaian kontrak, karena tipe rentang :: [Int] -> Int menjanjikan hasil untuk segenap list, sedangkan fungsinya hanya terdefinisi untuk list yang tidak kosong.
Memperbaiki kontrak properti#
Perbaikan yang tepat bukan menghapus propertinya, melainkan membuat pernyataannya jujur, yaitu rentang tidak pernah negatif untuk list yang tidak kosong. QuickCheck menyediakan pembungkus NonEmptyList pada modul Test.QuickCheck.Modifiers untuk kebutuhan semacam ini, sehingga pembangkit kasusnya berhenti menghasilkan list kosong. Properti kedua ditambahkan sebagai pemeriksaan silang, yaitu menggandakan setiap elemen menggandakan rentangnya, hubungan yang ikut terganggu apabila implementasi rentang berubah perilaku.
-- Properti dengan list tidak kosong setelah temuan kasus tepi
import Test.QuickCheck
import Test.QuickCheck.Modifiers (NonEmptyList (..))
import Statistik
-- rentang tidak pernah negatif untuk list tidak kosong
prop_rentangTidakNegatif :: NonEmptyList Int -> Bool
prop_rentangTidakNegatif (NonEmpty xs) = rentang xs >= 0
-- menggandakan setiap elemen menggandakan rentangnya
prop_gandaRentang :: NonEmptyList Int -> Bool
prop_gandaRentang (NonEmpty xs) = rentang (map (* 2) xs) == 2 * rentang xs
main :: IO ()
main = do
quickCheck prop_rentangTidakNegatif
quickCheck prop_gandaRentang+++ OK, passed 100 tests.
+++ OK, passed 100 tests.Kedua properti kini lolos pada seratus kasus acak dalam setiap eksekusi. Bentuk NonEmptyList Int pada signature properti menyatakan precondition secara eksplisit, sehingga pembaca properti mengetahui batas keberlakuannya tanpa membaca isi fungsinya. Apabila precondition semacam ini perlu diangkat menjadi tipe tersendiri, pola tipe tervalidasi seperti smart constructor menjadi langkah lanjutannya.
Kasus tetap sebagai unit test#
Properti menyatakan sifat yang berlaku luas, sedangkan unit test mengunci contoh konkret beserta angka yang sudah dihitung tangan. Kedua peran ini saling melengkapi, karena properti yang kurang tajam dapat lolos sambil membiarkan nilai keluaran salah, keadaan yang ditangkap oleh unit test yang menuntut angka persisnya. Suite berikut ditulis dengan tasty dan HUnit untuk mengunci dua nilai tersebut.
-- Suite tasty untuk modul Statistik
import Test.Tasty
import Test.Tasty.HUnit
import Statistik
main :: IO ()
main = defaultMain $ testGroup "Statistik"
[ testCase "rata-rata dua nilai" (rataRata [4, 6] @?= 5)
, testCase "rentang tiga nilai" (rentang [4, 6, 10] @?= 6)
]Statistik
rata-rata dua nilai: OK
rentang tiga nilai: OK
All 2 tests passed (0.00s)Keluaran tasty berupa pohon kasus beserta statusnya, dan baris All 2 tests passed menyatakan seluruhnya sehat. Kasus rata-rata dua nilai mengunci angka 5 untuk masukan [4, 6], sedangkan kasus rentang tiga nilai mengunci angka 6 untuk masukan [4, 6, 10]. Dua angka ini berfungsi sebagai pagar, yaitu setiap perubahan implementasi yang menggeser hasil akan dilaporkan pada titik pemeriksaan yang sama sebelum sempat menyebar ke bagian lain.
Tolok ukur pada akhir pengerjaan#
Titik pemeriksaan terakhir berupa tolok ukur yang mengukur biaya waktu, dan dikerjakan hanya setelah implementasi berhenti berubah, karena angkanya baru bermakna pada kode yang stabil. Program berikut membandingkan dua varian penjumlahan list dengan criterion, yaitu foldl yang membentuk thunk akumulasi dan foldl' yang memaksakan evaluasinya. Pasangan ini dipilih karena perilakunya dapat ditebak dari mekanisme thunk, sehingga angkanya menguji dugaan yang ada dan bukan kejutan.
-- tidak-dijalankan
-- benchmark dua varian penjumlahan dengan criterion
import Criterion.Main
import Data.List (foldl')
jumlahLambat :: [Int] -> Int
jumlahLambat = foldl (+) 0
jumlahCepat :: [Int] -> Int
jumlahCepat = foldl' (+) 0
main :: IO ()
main = defaultMain
[ bgroup "penjumlahan"
[ bench "foldl" (nf jumlahLambat [1 .. 100000])
, bench "foldl'" (nf jumlahCepat [1 .. 100000])
]
]Keluaran criterion berupa angka waktu yang berbeda pada setiap eksekusi, sehingga yang digunakan adalah bandingannya dan bukan angka persisnya. Pada salah satu eksekusi di sandbox jalur ini, foldl membutuhkan waktu sekitar 3,9 ms untuk seratus ribu elemen, sedangkan foldl' sekitar 0,8 ms, selisih sekitar lima kali lipat. Laporan criterion juga memuat jangkauan waktu, simpangan baku, dan koefisien determinasi yang menilai kestabilan pengukuran, ketiganya berguna untuk memastikan perbandingan tidak tertipu oleh kejadian yang jarang.
Kesalahan umum#
Kesalahan pertama adalah memperlakukan properti yang gagal sebagai hambatan pengerjaan, sehingga propertinya dihapus alih-alih dipelajari. Properti pertama justru menemukan ketidaksesuaian kontrak yang nyata, dan menghapusnya hanya memindahkan kegagalan yang sama kepada pengguna modul. Properti yang gagal merupakan keluaran kerja titik pemeriksaan, sama seperti unit test yang berstatus gagal ketika implementasi menyimpang dari kontraknya.
Kesalahan kedua berupa menggandakan pekerjaan yang sama pada dua alat, misalnya menulis puluhan unit test untuk kasus acak yang sudah tercakup oleh properti. Pembagian perannya sebaiknya dijaga, yaitu properti menampung sifat yang berlaku luas, sedangkan unit test mengunci angka konkret dan kasus tepi yang pernah ditemukan. Kesalahan ketiga berupa tolok ukur yang dijalankan sebelum implementasi stabil, keadaan yang menghasilkan angka yang tidak dapat dibandingkan antareksekusi, sehingga keputusan yang didasarkan pada angka tersebut ikut tidak dapat dipercaya.
Ringkasan#
- Properti menjadi titik pemeriksaan pertama dan menemukan ketidaksesuaian kontrak
rentangpada list kosong sebelum kontrak itu digunakan lebih jauh. - Precondition dinyatakan secara jujur melalui
NonEmptyList, sehingga properti yang gagal berubah menjadi properti yang lolos tanpa menghapus pernyataannya. - Unit test tasty mengunci nilai konkret yang dihitung tangan dan melengkapi properti yang menyatakan sifat luas.
- Tolok ukur criterion dikerjakan setelah implementasi stabil, dan yang digunakan adalah bandingannya, bukan angka absolutnya.
Referensi#
- Test.QuickCheck.Modifiers — Hackage
— Dokumentasi
NonEmptyListsebagai pembungkus precondition pada properti. - Test.Tasty — Hackage
— Dokumentasi
defaultMaindantestGroupuntuk menyusun suite pengujian. - Criterion Main — Hackage
— Dokumentasi
defaultMain,bench, dannfuntuk tolok ukur.