- Property-based testing menyebar ke berbagai bahasa setelah QuickCheck, tetapi per Juli 2024 banyak library masih belum menyediakan pengujian berbasis state dan pengujian paralel yang memadai, padahal keduanya sudah dirumuskan sejak 2009
- Kesenjangan utamanya ada pada kemampuan memverifikasi perubahan state berurutan dengan model state machine, lalu memakai ulang model yang sama untuk pemeriksaan linearisability guna menemukan race condition dalam eksekusi paralel
- Banyak objek yang disurvei tidak memiliki pengujian berbasis state atau masih eksperimental, dan pengujian paralel bahkan lebih jarang; issue terkait masih bertahan selama bertahun-tahun di FsCheck, Gopter, RapidCheck, SwiftCheck, jsverify, dan lainnya
- Implementasi Haskell sekitar 400 baris mereproduksi property-based testing berbasis state dan paralel, serta menggunakan implementasi referensi berbasis fake yang akrab bagi programmer sebagai model, alih-alih spesifikasi state machine tradisional
- Fake yang telah diuji kontrak dapat digunakan ulang bukan hanya untuk memverifikasi satu komponen, tetapi juga untuk pengujian integrasi yang cepat dan deterministik dengan menyuntikkannya sebagai pengganti dependensi nyata
Kesenjangan fitur setelah QuickCheck
- Property-based testing menyebar ke komunitas berbagai bahasa pemrograman dengan slogan “jangan tulis tes, hasilkan tes”
- Halaman Wikipedia untuk QuickCheck, library Haskell aslinya, mencantumkan 57 implementasi ulang dalam bahasa lain
- Makalah QuickCheck pertama, QuickCheck: A Lightweight Tool for Random Testing of Haskell Programs, dipresentasikan di ICFP 2000, dan seluruh source implementasi pertamanya berupa sekitar 300 baris kode di lampiran makalah
- QuickCheck awal hanya dapat menguji fungsi murni, sedangkan Testing monadic code with QuickCheck pada 2002 meletakkan dasar untuk menangani kode berefek seperti mutable state, file I/O, dan networking
Munculnya pengujian berbasis state dan paralel
- Quviq AB didirikan pada 2006 oleh John Hughes dan Thomas Arts, dengan pengujian proyek Erlang milik Ericsson sebagai salah satu kasus penggunaan awal
- Erlang bukan bahasa fungsional murni dan concurrency umum digunakan, sehingga QuickCheck monadic yang ada belum cukup nyaman dipakai
- Erlang QuickCheck closed source dari Quviq kemudian memuat dua fitur yang absen di banyak implementasi open source
- Property-based testing berbasis state secara sekuensial menggunakan model state machine
- Pengujian paralel yang memakai ulang model state machine sekuensial yang sama untuk mendeteksi race condition
- Pengujian berbasis state muncul dalam bentuknya saat ini di QuickCheck testing for fun and profit (2007)
- Pengujian paralel dibahas secara rinci dalam Finding Race Conditions in Erlang with QuickCheck and PULSE (ICFP 2009), dengan Linearizability: a correctness condition for concurrent objects (1990) dari Herlihy dan Wing sebagai teknik utamanya
- Kode library Quviq QuickCheck tidak dibagikan dalam makalah; yang dipublikasikan hanya API dan contoh tes yang menggunakan API tersebut
Hasil survei library tahun 2024
- State-of-the-art saat ini adalah stateful testing berbasis model state machine dan parallel testing yang menggabungkan linearisability dengan model sekuensial yang sama
- Survei ini merupakan hasil membaca dan merangkum dokumentasi, issue tracker, serta sebagian source code per Juli 2024
- Banyak library tidak menyediakan pengujian berbasis state atau hanya menyediakannya secara terbatas
- QuickCheck(Haskell) memiliki issue penambahan pengujian berbasis state yang terbuka sejak 2016
- SwiftCheck juga memiliki issue penambahan pengujian berbasis state yang terbuka sejak 2016
- jsverify masih memiliki issue penambahan pengujian berbasis state sejak 2015
- proptest(Rust) perlu merujuk ke proptest-state-machine terpisah
- Dukungan pengujian paralel lebih jarang lagi
- README Gopter menulis “No parallel commands … yet?” dan memiliki issue dari 2017
- FsCheck memiliki issue penambahan parallel support yang terbuka sejak 2016
- RapidCheck memiliki issue penambahan parallel support yang terbuka sejak 2015
- propcheck memiliki issue penambahan parallel testing sejak 2020
- Contoh open source yang mendukung kedua fitur tersebut meliputi PropEr, Hedgehog, qcheck-stm, quickcheck-state-machine, dan stateful-check
- Ada juga kasus yang tetap memiliki batasan meski punya fitur paralel
- Komentar source QuickTheories menyatakan bahwa pengujian paralelnya membuat jumlah end state yang mungkin bertambah cepat seiring jumlah command, sehingga command list biasanya harus dibatasi hingga 10 atau kurang
- Contoh LevelDB dan Redis dari ScalaCheck disajikan sebagai contoh sekuensial dengan
threadCount = 1 - Dukungan race condition di fast-check tampaknya berbeda dari pengujian paralel Quviq QuickCheck karena tidak terlihat memakai ulang model state machine sekuensial atau menggunakan linearisability
- Tidak terlihat contoh jelas pengujian paralel yang ditambahkan belakangan, dan jika tidak tercermin dalam desain API sejak awal, penambahan fitur ini mungkin memerlukan redesain yang cukup besar
Mengapa penyebaran fitur berjalan lambat
- John Hughes mengemukakan tiga alasan
- Pengujian berbasis state dan paralel tidak seberguna pengujian fungsi murni
- Menulis model state machine membutuhkan cara berpikir yang berbeda dari tes biasa dan perlu edukasi
- Open source saja tidak cukup mendorong adopsi industri; produk closed source serta pelatihan dan konsultasi membantu adopsi
- Menguji potongan fungsi murni dengan property-based testing saja sudah memberi banyak manfaat, tetapi sistem industri banyak berisi database, protokol stateful, dan struktur data concurrent, sehingga pengujian berbasis state dan paralel hampir sama pentingnya
- Spesifikasi berbasis state tidak selalu lebih sulit daripada spesifikasi fungsi murni
- Model key-value store dapat berjalan cukup jauh hanya dengan daftar pasangan key-value
- Dalam kasus LevelDB, model sederhana menemukan counterexample yang menyusut menjadi 17-step dalam beberapa menit, lalu setelah perbaikan Google, kembali menemukan counterexample 31-step dalam beberapa menit
- Masalah kedua adalah bug pada background compaction process; compaction penting untuk meningkatkan performa baca dan mengklaim kembali disk space, tetapi tidak dimasukkan secara eksplisit dalam model
- Meski closed source mungkin membantu adopsi industri, dinilai tidak membantu adopsi open source
- Untuk mereproduksi hasil makalah tanpa lisensi Quviq QuickCheck, diperlukan banyak reverse engineering sehingga dianggap hampir mustahil
Usulan: implementasi kecil yang terbuka dan spesifikasi yang mudah
- Arah perbaikannya ada dua
- Menyediakan implementasi open source singkat untuk property-based testing berbasis state dan paralel, seperti implementasi QuickCheck asli yang sekitar 300 baris
- Mengurangi beban penulisan spesifikasi dengan memakai ulang konsep mock dan test double yang sudah akrab bagi programmer, alih-alih state machine
- Untuk memverifikasi hipotesis ini, ditunjukkan dua hal
- Property-based testing berbasis state dan paralel diimplementasikan dalam sekitar 400 baris kode
- Alih-alih state machine, model yang digunakan adalah in-memory reference implementation, yaitu fake
Ringkasan property-based testing murni
- Dalam pengujian fungsi murni, kita menghasilkan input lalu memeriksa apakah output fungsi memenuhi relasi tertentu dengan input
- Misalnya,
reversedapat diuji dengan propertyreverse (reverse xs) == xsuntuk daftar arbitrerxs - QuickCheck secara default menghasilkan 100 pengujian, dan jika gagal, ia melakukan shrink pada input untuk menyajikan counterexample minimal
- Property yang keliru seperti
reverse xs == xsakan diperkecil menjadi contoh tandingan minimal seperti[0,1] - Pola property yang sering muncul meliputi inverse, idempotency, associativity, axiom dari abstract data type, metamorphic property, dan sebagainya
- inverse:
deserialise (serialise i) == i - idempotency:
sort (sort xs) == sort xs - associativity:
(i + j) + k == i + (j + k)
- inverse:
Property-based testing berbasis state
- Komponen yang memiliki state tidak selalu menghasilkan output yang sama untuk input yang sama
- Hasil
incrpertama pada counter dan hasilincrkedua berubah bergantung pada state sebelumnya - Database dan file system juga membuat riwayat input sebelumnya memengaruhi output berikutnya
- Hasil
- Jika pengujian fungsi murni menangani satu input, pengujian berbasis state menghasilkan urutan input untuk memeriksa bagaimana sistem berubah seiring waktu
- Model direpresentasikan sebagai fake berbentuk
m -> i -> (m, o)- Dari state model sebelumnya
mdan inputi, ia menghitung model berikutnya dan outputo - Output sistem nyata dibandingkan dengan output fake pada setiap langkah
- Jika ada ketidakcocokan, urutan input di-shrink untuk mencari counterexample kecil
- Dari state model sebelumnya
-
Contoh Counter
- Counter Haskell yang menggunakan variabel mutable global dijadikan target pengujian
incrmenaikkan counter, sedangkangetmembaca nilai saat ini- Model cukup berupa satu
Counter Int, dan instanceStateModelmendefinisikan state awalCounter 0,Incr,Get,Incr_ (),Get_ Int,runFake,runReal, serta generator command - Jika bug seperti
incr42Bug, yang tidak bertambah saat nilai counter 42, dimasukkan, QuickCheck menemukan kegagalan setelah 66 pengujian dan, setelah 29 kali shrink, menyajikan contoh tandingan minimal berupaGetsetelah 43 kali increment - Jika counter global nyata tidak di-
resetdi antara pengujian, model selalu mulai dari 0 tetapi counter nyata mempertahankan state pengujian sebelumnya, sehingga terjadi mismatch
-
Antarmuka library berbasis state
- Antarmuka
StateModelmemandang sistem yang diuji sebagai black box, dengan command sebagai input dan response sebagai output - Komponen intinya adalah
Command state,Response state,initialState,runFake,runReal,generateCommand - Komponen opsionalnya adalah sebagai berikut
Reference: digunakan saat command berikutnya perlu merujuk resource yang dibuat oleh response sebelumnya, seperti file handlePreconditionFailure: merepresentasikan kegagalan precondition, misalnya mencegah read dari handle yang bukan file terbukaCommandMonad: default-nyaIO, tetapi monad lain dapat digunakanmonitoring,commandName: digunakan untuk coverage dan statistik
- Saat membuat command, nilai seperti file handle nyata tidak bisa dibuat, sehingga dibuat symbolic reference berbentuk
Var Int, lalu selama eksekusi diganti dengan reference nyata - Setelah shrink, command yang melanggar precondition atau menggunakan symbolic reference di luar scope akan dihapus
- Antarmuka
-
Contoh circular buffer
- Circular queue yang ditulis dalam C diuji melalui Haskell FFI, dan modelnya ditulis sebagai queue berbasis daftar sederhana
- Implementasi C tidak melakukan error checking, sehingga
getpada queue kosong dapat mengembalikan memory yang belum diinisialisasi - Implementasi nyata efisien dengan circular index tetapi tidak jelas benar, sedangkan fake kurang efisien tetapi tidak menjadi masalah karena hanya untuk pengujian
- Karena
newmengembalikan reference queue, model mengelola beberapa queue denganMap (Var Queue) FQueue - Awalnya precondition untuk
putpada queue penuh terlewat; saat0dan1dimasukkan ke queue berukuran 1 laluget, model mengharapkan0karena FIFO, tetapi kode C mengembalikan1 - Ini bukan bug implementasi, melainkan precondition model yang kurang, sehingga diperbaiki dengan menambahkan precondition
QueueIsFull - Tidak adanya command
Sizedi generator terungkap dari output coverage, dan setelah ditambahkan, bug perhitungan ukuran queue ditemukan - Saat satu item dimasukkan ke queue berukuran 1 lalu
Size, nilai yang diharapkan adalah 1 tetapi nilai aktual 0; perbaikan yang diajukan adalah mengatur ukuran buffer internal dinewmenjadin + 1 - Setelah itu,
abs(q->inp - q->outp) % q->sizelolos untuk ukuran 1 tetapi kembali gagal untuk ukuran 2, dan perbaikan akhirnya adalah(q->inp - q->outp + q->size) % q->size
-
Teka-teki jeriken air Die Hard 3
- Teka-teki membuat tepat 4L air dengan jeriken 3L dan 5L diselesaikan dengan pengujian berbasis state
- Bahkan tanpa implementasi nyata, hanya dengan menjalankan model dan fake, ketika state tertentu tercapai pengujian dapat dibuat gagal untuk memperoleh urutan action yang sudah di-shrink
- Setelah 199 pengujian dan 11 kali shrink, sequence yang disajikan adalah alur berikut
- Isi jeriken 5L
- Tuang dari 5L ke 3L
- Kosongkan jeriken 3L
- Tuang lagi dari 5L ke 3L
- Isi jeriken 5L
- Tuang dari 5L ke 3L
- Trace menampilkan state perantara sehingga proses big jug menjadi 4L dapat diperiksa
Pengujian berbasis properti paralel
- Bug pada concurrent code sulit direproduksi dan diverifikasi perbaikannya karena thread interleaving berbeda di setiap eksekusi
- Tujuannya adalah memungkinkan pengguna melakukan pengujian paralel seperti pengujian berbasis state sekuensial, tanpa perlu menulis banyak kode pengujian tambahan
- Pada contoh counter, jika
incrmenjalankanwriteIORefsetelahreadIORefsecara non-atomik, dua thread dapat saling menimpa increment satu sama lain sehingga terjadi race condition - Pengujian paralel mengumpulkan waktu invocation dan response dari command selama eksekusi untuk membuat concurrent history, lalu memeriksa apakah history itu dapat dijelaskan oleh suatu interleaving sekuensial
- Jika ada satu saja interleaving yang cocok dengan model sekuensial, history dianggap linearise dan dinilai correct
- Jika tidak ada interleaving sekuensial yang dapat menjelaskan response aktual, hasilnya diperlakukan sebagai non-linearisable
-
Pembuatan command paralel dan shrink
- Program paralel direpresentasikan dengan
ParallelCommandsdan beberapaFork, dan command di dalam setiapForkdijalankan secara paralel - Implementasi contoh menangani single, double, dan triple threaded execution
- Dalam eksekusi paralel, seperti
Fork [Write "a" "foo", Write "a" "bar"], state model yang mungkin dapat berbeda tergantung interleaving - Model paralel melakukan pembuatan command dan shrink berdasarkan himpunan state, bukan satu state tunggal
parallelSafememeriksa apakah precondition tetap terjaga pada semua permutation command di dalamFork- Misalnya, jika
Write "a"danDelete "a"berada dalam fork yang sama, satu command dapat merusak precondition command lainnya - Dalam proses shrink juga hanya command yang mempertahankan precondition dan symbolic reference scope yang disisakan
- Program paralel direpresentasikan dengan
-
Eksekusi paralel dan pemeriksaan linearisability
- Eksekusi paralel mencatat event
InvokedanOkdari setiap command sebagai history - Jika response berisi reference baru, lingkungan diperluas dengan atomic counter untuk menghindari tabrakan nomor reference antar-thread
- Semua interleaving yang mungkin dari history dienumerasi sebagai tree
Rose linearisablememeriksa apakah ada path dalam tree ini yang mencocokkan response dengan modelrunFakesekuensial- Pada akhirnya pengujian paralel menggunakan kembali model sekuensial, sehingga pengguna cukup menulis model sekuensial lalu mendapatkan pengujian paralel dengan sedikit kode tambahan
- Eksekusi paralel mencatat event
-
Contoh parallel counter
- Kode yang ditambahkan untuk mengaktifkan pengujian paralel pada counter hanyalah instance
ParallelModel Counterdan property - Jika memakai
incrRaceConditionyang non-atomik, race condition ditemukan - Sekalipun ada race pada test case yang lebih kecil, jika kegagalan tidak tereproduksi karena interleaving lain, QuickCheck dapat menganggap test case yang lebih kecil lolos dan menghentikan shrink
- Solusi yang tepat adalah deterministic thread scheduler, dan makalah pengujian paralel menggunakan ini
- Implementasi contoh memakai workaround yang lebih sederhana: menambahkan sleep singkat di sekitar read/write shared memory agar peluang interleaving yang sama terjadi meningkat
- Sleep diperlukan bukan untuk menemukan race, melainkan untuk memperkecil counterexample dari race yang sudah ditemukan
- Setelah sleep ditambahkan, counterexample minimum menyusut menjadi
ParallelCommands [Fork [Incr,Incr],Fork [Get]]
- Kode yang ditambahkan untuk mengaktifkan pengujian paralel pada counter hanyalah instance
-
Contoh process registry
- Sebagai contoh digunakan sistem seperti Erlang process registry, yang men-spawn thread dan melakukan register·lookup·unregister·kill terhadap
ThreadIdberdasarkan nama - Model sekuensial melacak thread id yang dibuat, pasangan name-thread yang terdaftar, dan thread id yang telah di-kill
- Karena
RegisterdanUnregisterdapat gagal, response menggunakanEither ErrorCall () - Informasi error location dari implementasi nyata dihapus dengan
abstractErroragar cocok dengan fake monitoringmenampilkan coverageRegisterFailed,RegisterSucceeded,UnregisterFailed,UnregisterSucceeded- Jika sengaja memasukkan bug yang membuat
registermenimpa registry yang sudah ada, muncul counterexample sekuensial yang tidak dapat meng-unregister"e"yang sudah terdaftar - Dalam pengujian paralel muncul counterexample yang lebih panjang, dan jika memakai
SleepyIORef, ia menyusut menjadi bentukFork [Register "b" (Var 0), Register "c" (Var 0)] - Masalahnya adalah race ketika thread lain dapat menyela di antara pemeriksaan dengan
readRegistrydan pemanggilanatomicModifyIORef - Setelah menerapkan global lock pada
register,unregister, dankill, pengujian paralel lolos
- Sebagai contoh digunakan sistem seperti Erlang process registry, yang men-spawn thread dan melakukan register·lookup·unregister·kill terhadap
Model Berbasis Fake dan Pengujian Integrasi
- Alih-alih menggunakan spesifikasi state machine tradisional dengan post-condition, digunakan fake in-memory sebagai reference implementation
- Tulisan Edsko de Vries tahun 2019 diperkenalkan sebagai tulisan pertama yang mengusulkan cara mengimplementasikan fake di atas spesifikasi state machine berbasis post-condition
- Fake mirip dengan mock, sehingga diajukan sebagai pendekatan yang lebih mudah bagi programmer yang tidak terbiasa dengan formal specification
- Fake juga punya keunggulan karena dapat digunakan sebagai pengganti komponen dependensi dalam integration test
- Tidak perlu memulai atau mengaktifkan dependency nyata
- Dapat menyusun integration test yang lebih cepat dan deterministic
- Masalah bahwa fake bisa salah ditangani dengan contract test
- Karena property-based test berbasis status dan paralel memverifikasi kesesuaian antara fake dan implementasi nyata, fake berperan sebagai dependensi yang telah diuji kontraknya
-
Memisahkan Pengujian dan Deployment dengan Fake Queue
- Interface queue
IQueuememilikiiNew,iPut,iGet,iSize - Implementasi nyata langsung menghubungkan wrapper C queue
- Implementasi fake menyimpan status model di
IORefdan memperbaruinya melaluifNew,fPut,fGet,fSize - Komponen ditulis terhadap interface
IQueue q - Dalam pengujian digunakan instance
fake, sedangkan dalam deployment digunakan instancereal - Dengan property-based test berbasis status, dibangun asumsi bahwa fake faithful terhadap real
- Interface queue
-
Fake File System
- Interface file system
IFileSystem hmemilikiiMkDir,iOpen,iWrite,iClose,iRead - Implementasi nyata menggunakan file system sungguhan di bawah
/tmp/qc-test - Fake diimplementasikan sebagai
FakeFSin-memory yang memiliki directory set, file content map, open handle map, dan next handle fOpen,fWrite,fClose,fReadmemodelkan precondition failure seperti file yang sedang busy, directory yang tidak ada, dan handle yang sudah ditutup- Jika fake file system telah diuji faithful terhadap file system nyata, komponen yang bergantung pada file system dapat diuji integrasinya dengan fake lalu diganti dengan file system nyata saat deployment
- Jika muncul bug setelah diganti ke real, perlu diselidiki bagaimana mismatch antara fake dan real bisa lolos dari property-based test berbasis status
- Interface file system
-
Sistem Komponen yang Lebih Besar
- Sistem tempat A bergantung pada B dan B bergantung pada C juga diperluas dengan cara yang sama
- Setiap komponen diberi interface
iC :: IO ICiB :: IC -> IO IBiA :: IB -> IO IA
- Strategi pengujiannya adalah sebagai berikut
- Memverifikasi C dengan property-based test berbasis status dan paralel untuk memperoleh fake C yang telah diuji kontraknya
- Dalam integration test B, gunakan fake C
- Dalam pengujian A, gunakan fake B yang menggunakan fake C
- Cara ini diperluas ke lebih banyak komponen atau layanan dengan pola yang sama
Kesimpulan
- Property-based testing berbasis status dan paralel dapat diimplementasikan dengan sekitar 400 baris kode, ukuran yang sebanding dengan implementasi QuickCheck pertama tanpa shrinking yang sekitar 300 baris
- Jika fake digunakan sebagai model, penulisan spesifikasi untuk pengujian berbasis status dan paralel menjadi bentuk yang lebih familier, dan dapat digunakan kembali untuk menguji sistem yang lebih besar secara compositional
- Jika setiap komunitas bahasa terus bereksperimen, masih ada ruang untuk memperbaiki kondisi library property-based testing
1 komentar
Komentar Hacker News
Fuzzing berbasis coverage sudah hadir dan juga didukung dengan baik di Go, jadi penasaran apa yang terlewat jika tidak memakai pustaka pengujian berbasis properti
https://www.tedinski.com/2018/12/11/fuzzing-and-property-tes...
Melihat fuzz test di bawah dan pemeriksaan invariant yang terkait, rasanya ini pada dasarnya hampir sama dengan property test
https://github.com/ncruces/aa/blob/505cbbf94973042cc7af4d6be...
https://github.com/ncruces/aa/blob/505cbbf94973042cc7af4d6be...
Memang ada perbedaan nyata, tetapi batasnya cukup kabur, dan tidak terlalu penting untuk menentukan secara persis mana yang fuzzing dan mana yang pengujian berbasis properti
Tes yang berjalan cepat dengan assertion terperinci adalah pengujian berbasis properti; yang berjalan lama dan hanya mencari crash adalah fuzzing; yang di antaranya bersifat ambigu
https://hypothesis.works/articles/what-is-property-based-tes...
Saat di Google, ada alat internal yang menggabungkan keduanya dan itu sangat bagus. Anda menulis pengujian berbasis properti seperti biasa, lalu saat dijalankan framework pengujian mengompilasi secara khusus untuk mendapatkan coverage dan menyesuaikan input acak agar coverage meningkat. Tentu saja semuanya berjalan sepenuhnya otomatis di klaster banyak mesin
Pengujian berbasis properti tradisional biasanya diimplementasikan hanya sebagai pustaka, jadi belum tentu memiliki informasi coverage untuk memandu pembuatan input acak
Namun, bergantung pada pustakanya, Anda bisa mendapatkan cukup banyak fitur kemudahan. Salah satu yang berguna adalah shrinking, dan bisa melihat bagian “Shrinking” di sini: https://tech.fpcomplete.com/blog/quickcheck-hedgehog-validit...
Kombinator untuk menyusun generator juga bagus, dan bergantung pada pustakanya, ada juga yang memiliki kumpulan nilai “buruk” yang diketahui dapat memicu perilaku pengecualian
Saya ingin mundur selangkah dan mengajukan pertanyaan yang lebih meta tentang pengujian. Apakah tes yang berhasil berarti kode berhasil, dan sebaliknya juga berlaku? Apakah kontrak Go menyatakan bahwa jika input yang sama dimasukkan ke kode yang sama, output yang sama akan dihasilkan?
Dengan menangani tipe Arbitrary yang merepresentasikan himpunan objek acak, kita bisa dengan mudah menulis fungsi reusable untuk menghasilkan input pengujian. Pustaka seperti itu tampaknya juga bisa dipakai bersama framework fuzzing Go dengan cukup mudah
Namun saya rasa kombinator umum seperti map, filter, chain, dan oneOf bisa terasa agak canggung, jadi saya sedang menulis pustaka pengujian properti baru untuk JavaScript. Tujuannya membuatnya lebih nyaman digunakan, tetapi masih eksperimental dan belum dirilis ke publik
clojure.spec.alphaadalah pengalaman yang luar biasa, baik dipakai bersamatest.checkmaupun tidak, tetapi ketika saya mencobahypothesisdi Python, hasilnya benar-benar burukHypothesis tampaknya secara desain tidak mampu menangani kumpulan data yang sederhana tetapi “besar”, dan “besar” di sini sebenarnya tidak terlalu besar. [0] Saking menyakitkannya, kami mencabut Hypothesis dan pengujian berbasis generasi sepenuhnya dari rangkaian pengujian Python di tempat kerja
[0] https://github.com/HypothesisWorks/hypothesis/issues/3493
Hypothesis mencoba mengecilkan integer yang dihasilkan menjadi 0 untuk melihat apakah bug juga ada pada 0, dan pengujiannya bukan gagal karena berisi 0, melainkan menolaknya. Pada contoh kecil ini hanya menjadi ketidakefisienan, tetapi pada contoh besar sampai membuat Hypothesis menyerah
Di thread itu, seseorang menyarankan memakai strategi pembuatan instance lain yang tidak dapat menghasilkan 0. Caranya adalah tidak menghasilkan nilai favorit shrinker Hypothesis sejak awal, alih-alih menghasilkannya lalu menolaknya. Saya penasaran apakah itu sudah dicoba
Saya juga penasaran bagaimana
clojure.spec.alphamenangani hal ini secara berbedaKomentar mjaniczek di https://news.ycombinator.com/item?id=40876437 menyebut kasus ini sebagai kelemahan pendekatan Hypothesis
Intinya: “Karena generator kini menjadi parser daftar byte yang bisa gagal, ada sedikit ketidakefisienan, dan pengguna bisa membuat generator aneh yang tidak dapat diperkecil sempurna oleh shrinker internal. Meski begitu, dari tiga pendekatan itu, pengalaman developernya paling baik…”
Tentu saja, saya rasa orang itu tidak akan setuju bahwa pengujiannya sendiri ditulis dengan cara yang “aneh”
filterdengan cara yang menimbulkan masalahnya sendiriJika Anda membuat data secara acak lalu menyaring yang sesuai dengan suatu properti, pada dasarnya Anda sedang menggosok tiket lotre dalam proses generasinya
Jawaban sederhana untuk pertanyaan dalam tulisan itu, “Mengapa tidak ada tuntutan bahwa riset yang diterbitkan harus dapat direproduksi dengan alat open source, atau setidaknya alat yang tersedia gratis bagi publik dan peneliti lain?”, adalah bahwa konsekuensi langsung dari tuntutan seperti itu ialah makalah yang tidak memenuhi syarat tersebut tidak akan diterbitkan
Misalnya, makalah Quviq QuickCheck yang tampaknya berguna bagi penulisnya dan orang lain pun kemungkinan tidak akan diterbitkan, dan komunitas akan kehilangan hadiah berupa informasi tersebut
Setiap persyaratan punya efek mengecualikan, dan selalu ada kasus batas berupa makalah yang tetap bisa berguna meskipun tidak memenuhi persyaratan
Jika argumen ini dianggap valid, orang bisa memakainya sebagai tameng untuk melangkah sejauh apa pun. Jika reprodusibilitas dibuang dari persyaratan, maka tidak perlu menjelaskan apa pun yang tidak ingin dijelaskan. Tidak perlu menyediakan data tentang sampel, tidak perlu uji signifikansi statistik. Abstrak samar yang mengklaim telah mencapai suatu hasil saja sudah cukup
Bahkan catatan terkenal Fermat di margin salinan pribadi Arithmetica pun akan menjadi makalah riset yang sepenuhnya valid. Tentu kita tidak ingin kehilangan informasi berharga bahwa seorang matematikawan terkenal mengira ia punya pembuktian yang ringkas dan elegan untuk suatu teorema. Meski tentu saja, pada kenyataannya kemungkinan besar ia tidak memilikinya
Pandangan saya tentang pertanyaan politis ini adalah bahwa standar saat ini terlalu longgar. Tidak ada orang yang dipaksa menerbitkan sesuatu. Di dunia ada banyak riset yang tidak diterbitkan di mana pun karena alasan seperti nilai proprietari, dan riset semacam itu tidak akan hilang
Namun jika bekerja di akademia, terlebih lagi menerima dana riset, dan mengatakan bahwa tujuannya adalah memajukan pengetahuan ilmiah dunia, maka adil untuk menuntut agar benar-benar mengikuti tujuan itu. Jangan sekadar berpura-pura mengikuti tujuan tersebut demi menaiki tangga karier akademik
Sertakan juga semua yang diperlukan untuk menjalankan kodenya. Bisa jadi ini sudah dilakukan
Untuk tujuan itu, kecil kemungkinan jumlah makalah yang diterbitkan akan turun hanya karena persyaratannya bertambah
Masalah yang lebih serius pada makalah yang diterbitkan adalah seringnya kesalahan sengaja dibiarkan lewat demi menerbitkan sebanyak dan secepat mungkin. Jika verifikasi makalah menjadi lebih mudah, situasinya mungkin membaik, tetapi saya tidak akan terlalu berharap. Orang sangat mahir mencari jalan pintas
Saya cukup sering menulis stateful property test dengan
proptestdi Rust, dan biasanya cukup sederhana kalau dikodekan sendiriContoh nontrivial yang menemukan 6 bug ada di https://github.com/sunshowers-code/buf-list/blob/main/src/cu...
Pengujian paralel kadang bisa berguna, tetapi sering kali lebih mudah menjalankan banyak test secara paralel saja
Di level teratas memakai randomness sungguhan, lalu di bawahnya ada beberapa nested loop yang naik dari kasus berkompleksitas rendah ke kasus berkompleksitas tinggi. Setelah itu membuat seed untuk generator pseudorandom deterministik dan mencetaknya. Jika test gagal, cukup salin-tempel seed error untuk mereproduksi kasus gagalnya
Saya merasa property test manual seperti ini lebih cepat, lebih fleksibel, dan secara keseluruhan tidak terlalu merepotkan dibanding framework atau library mana pun
Namun untuk pengujian konkurensi yang benar-benar kokoh, saya sangat merekomendasikan library AWS Shuttle (https://github.com/awslabs/shuttle). Library ini bisa menemukan race condition yang luar biasa kompleks. Saya juga menulis tutorial kecil: https://grantslatton.com/shuttle
Di AWS, library ini digunakan untuk memverifikasi file system kustom yang ditulis untuk menjalankan AWS S3
Saya membaca cepat paper “Testing Telecoms Software with Quviq QuickCheck” yang ditautkan, tetapi tidak langsung terlihat jawaban atas pertanyaan “mengapa tidak lebih baik membuat sendiri operasi stateful ini?”
Teks aslinya menunjuk ke bagian ini dengan model pasangan key-value dari key-value store, tetapi saya tidak mengerti mengapa tidak cukup menulis state machine saja, atau mengapa framework diperlukan. Minggu lalu di tempat kerja saya benar-benar melakukan hal seperti ini untuk menguji interaksi file system, dan akhirnya kira-kira menjadi
type Instruction = | Read of stuff | Write of stuff | Seek of stuff | …Maka propertinya menjadi “dengan daftar perintah ini, …”. Bentuk
StateModelpada dasarnya juga meminta hal yang sama. Sulit melihatStateModelbenar-benar memberi banyak nilai; tampaknya hanya menghilangkan sangat sedikit kode test dalam pengalaman nyata, dengan imbalan menambahkan jauh lebih banyak kode framework yang harus dipahamiJika ingin hanya menghasilkan urutan transisi state yang “valid”, biasanya diperlukan state model yang menentukan langkah test mana yang valid pada state tertentu. Selain itu, selama shrinking, saat langkah test dihapus, jangan sampai prasyarat yang dipatuhi saat masing-masing langkah awalnya dibuat menjadi rusak dan menimbulkan kegagalan palsu
Jika yang diinginkan hanyalah urutan operasi acak sepenuhnya, di mana operasi apa pun valid pada state apa pun, framework
proptestyang stateful mungkin berlebihan. Namun jika harus mempertahankan state model dan menetapkan prasyarat untuk berbagai operasi, framework khusus banyak meringankan pekerjaanTahun lalu saya menulis posting blog tentang topik ini; kalau ingin contoh yang lebih mendalam, bisa dirujuk: https://readyset.io/blog/stateful-property-testing-in-rust
Seperti yang dikatakan orang lain, pengujian state machine paralel juga merupakan manfaat bagus yang bisa didapat dari framework khusus, tetapi bukan satu-satunya manfaat
Gaya testing boleh dicampur. Toh itu kode sendiri
Itulah manfaatnya
Penulis berfokus pada aspek state machine dan paralel dalam property-based testing, tetapi ada juga aspek lain yang mungkin memberi dampak lebih besar
Salah satunya adalah property-based testing yang dipandu cakupan, dan Anda bisa melihat tulisan Dan Luu: https://danluu.com/testing/
Satu lagi adalah bidang yang saya condongi, yaitu mengotomatiskan shrinking sambil mempertahankan semua invariant yang dibuat saat menghasilkan nilai
Singkatnya, fungsi shrinking turunan ala QuickCheck yang bekerja pada nilai (
shrink : a -> [a]) punya kendala dan masalah, sehingga orang akhirnya mematikan shrinking alih-alih menangani masalahnya“Integrated shrinking” dengan rose tree (misalnya Hedgehog) mengikuti constraint dari generator, tetapi bermasalah pada monadic bind, yaitu ketika hasil generator digunakan untuk bercabang ke generator lain
Satu-satunya pendekatan yang tampak secara ajaib “langsung jalan” adalah internal shrinking milik Hypothesis. Pendekatan ini memakai lapisan tidak langsung yang mengecilkan daftar pilihan acak, bukan nilai itu sendiri. Kekurangannya adalah generator kini menjadi parser daftar byte yang bisa gagal sehingga menimbulkan sedikit inefisiensi, dan pengguna bisa membuat generator aneh yang tidak bisa diperkecil secara sempurna oleh shrinker internal. Meski begitu, dari tiga pendekatan tersebut, pengalaman developernya paling baik, dan mengingat fakta bahwa orang benar-benar menulis test saja sudah seperti keajaiban kecil, sebagai penulis library testing pendekatan ini terasa paling layak dibangun
Sebagai gantinya, untuk shrinking yang optimal, sebaiknya lebih memilih generator applicative: https://github.com/hedgehogqa/haskell-hedgehog/issues/473#is...
Dengan kata lain, generator applicative tidak “menggunakan hasil generator untuk bercabang ke generator lain”, dan karena sifat “paralel” dari applicative, shrinking dapat dioptimalkan. Paralel di sini bukan dalam arti threading seperti di artikel, melainkan dalam arti monadik. Karena applicative bersifat “paralel”, generator dapat diperkecil secara independen. Sebaliknya, generator monadik bersifat “serial”, sehingga mengecilkan satu generator pasti mengubah perilaku generator berikutnya
Kalau ada presentasi publiknya, saya ingin melihat tautannya
Saya tidak menganggapnya benar-benar siap untuk produksi, dan itu pun tampaknya memang demikian secara desain.[0]
Saya punya cukup banyak pengalaman menggunakan
clojure.spec.alphadengan maupun tanpatest.check, jadi meskipun ada perbedaan, saya tidak sepenuhnya asing dengan ide umumnya[0] https://github.com/HypothesisWorks/hypothesis/issues/3493
Mirip dengan Hypothesis, tetapi menggunakan tree generator, bukan urutan linear. Ini berbasis selective functor, yang merupakan antarmuka bagus dan juga berguna untuk hal-hal seperti validator
Menurut https://hackage.haskell.org/package/falsify, library ini menyediakan property-based testing yang mendukung internal integrated shrinking. Integrated dalam arti Hedgehog, yaitu tidak perlu menulis shrinker dan generator terpisah; dan internal dalam arti Hypothesis, yaitu bekerja dengan baik bahkan di seluruh monadic bind
Saya pernah mencoba memakai property-based testing, tetapi selalu merasa terjepit di antara dua kursi
Jika saya memahami suatu properti cukup baik untuk mengujinya secara ketat, biasanya saya bisa memasukkannya ke type system agar benar by construction. Jika saya hanya ingin smoke test sederhana, satu input acak lebih mudah
Misalnya, sering kali ada dua implementasi: implementasi naif yang lambat tetapi sederhana dan implementasi teroptimasi, lalu kita bisa membandingkan output keduanya untuk input acak. Itu properti sederhana yang mudah dipahami, tetapi umumnya sulit dimasukkan ke type system
Demikian pula, urutan penyajian input semestinya tidak penting, atau mungkin ada cara untuk membagi data sehingga berlaku sifat seperti
max(nilai maksimum A, nilai maksimum B) = maximum(A union B). Bagaimana hal semacam itu bisa dienkode ke dalam type system?Atau ada hal seperti “untuk sembarang A dan B, solusi optimal apa pun yang ditemukan pada A lebih buruk daripada solusi optimal apa pun yang ditemukan pada A union B”, atau idempotensi seperti
f(f(A)) = f(A)Semua ini adalah properti yang mudah dipahami, tetapi tidak mudah dijelaskan dalam sebagian besar type system
Namun ada banyak constraint yang tidak bisa ditangani type checker arus utama. Dependent type akan sangat membantu, tetapi sejauh ini tampaknya masih terbatas pada ceruk seperti theorem prover
Saya bertanya-tanya apakah QuviQ Erlang QuickCheck yang asli terlewat dari daftar
Produk lengkapnya proprietary, tetapi versi gratis QuickCheck Mini juga tersedia: http://www.quviq.com/downloads/
Clojure sekarang juga punya library quickcheck yang stateful: https://github.com/griffinbank/test.contract
Testing paralel menarik, tetapi sejauh ini belum menjadi sumber rasa sakit yang besar
Untuk pengujian C#/.NET, saya sudah memakai CsCheck[0] dan cukup puas
Jauh lebih mudah didekati dibanding Hedgehog atau FsCheck, dan kecepatannya juga cukup tinggi
[0] https://github.com/AnthonyLloyd/CsCheck
Pengujian linearizability/paralel yang dijelaskan di tulisan itu juga didukung
Referensi:
https://github.com/AnthonyLloyd/CsCheck?tab=readme-ov-file#m...
https://github.com/AnthonyLloyd/CsCheck?tab=readme-ov-file#c...
Rasanya masuk akal kalau ada varian C# tersendiri