1 poin oleh GN⁺ 2024-07-06 | 1 komentar | Bagikan ke WhatsApp
  • 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, reverse dapat diuji dengan property reverse (reverse xs) == xs untuk daftar arbitrer xs
  • QuickCheck secara default menghasilkan 100 pengujian, dan jika gagal, ia melakukan shrink pada input untuk menyajikan counterexample minimal
  • Property yang keliru seperti reverse xs == xs akan 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)

Property-based testing berbasis state

  • Komponen yang memiliki state tidak selalu menghasilkan output yang sama untuk input yang sama
    • Hasil incr pertama pada counter dan hasil incr kedua berubah bergantung pada state sebelumnya
    • Database dan file system juga membuat riwayat input sebelumnya memengaruhi output berikutnya
  • 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 m dan input i, ia menghitung model berikutnya dan output o
    • Output sistem nyata dibandingkan dengan output fake pada setiap langkah
    • Jika ada ketidakcocokan, urutan input di-shrink untuk mencari counterexample kecil
  • Contoh Counter

    • Counter Haskell yang menggunakan variabel mutable global dijadikan target pengujian
    • incr menaikkan counter, sedangkan get membaca nilai saat ini
    • Model cukup berupa satu Counter Int, dan instance StateModel mendefinisikan state awal Counter 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 berupa Get setelah 43 kali increment
    • Jika counter global nyata tidak di-reset di antara pengujian, model selalu mulai dari 0 tetapi counter nyata mempertahankan state pengujian sebelumnya, sehingga terjadi mismatch
  • Antarmuka library berbasis state

    • Antarmuka StateModel memandang 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 handle
      • PreconditionFailure: merepresentasikan kegagalan precondition, misalnya mencegah read dari handle yang bukan file terbuka
      • CommandMonad: default-nya IO, tetapi monad lain dapat digunakan
      • monitoring, 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
  • 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 get pada 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 new mengembalikan reference queue, model mengelola beberapa queue dengan Map (Var Queue) FQueue
    • Awalnya precondition untuk put pada queue penuh terlewat; saat 0 dan 1 dimasukkan ke queue berukuran 1 lalu get, model mengharapkan 0 karena FIFO, tetapi kode C mengembalikan 1
    • Ini bukan bug implementasi, melainkan precondition model yang kurang, sehingga diperbaiki dengan menambahkan precondition QueueIsFull
    • Tidak adanya command Size di 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 di new menjadi n + 1
    • Setelah itu, abs(q->inp - q->outp) % q->size lolos 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 incr menjalankan writeIORef setelah readIORef secara 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 ParallelCommands dan beberapa Fork, dan command di dalam setiap Fork dijalankan 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
    • parallelSafe memeriksa apakah precondition tetap terjaga pada semua permutation command di dalam Fork
    • Misalnya, jika Write "a" dan Delete "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
  • Eksekusi paralel dan pemeriksaan linearisability

    • Eksekusi paralel mencatat event Invoke dan Ok dari 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
    • linearisable memeriksa apakah ada path dalam tree ini yang mencocokkan response dengan model runFake sekuensial
    • Pada akhirnya pengujian paralel menggunakan kembali model sekuensial, sehingga pengguna cukup menulis model sekuensial lalu mendapatkan pengujian paralel dengan sedikit kode tambahan
  • Contoh parallel counter

    • Kode yang ditambahkan untuk mengaktifkan pengujian paralel pada counter hanyalah instance ParallelModel Counter dan property
    • Jika memakai incrRaceCondition yang 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]]
  • Contoh process registry

    • Sebagai contoh digunakan sistem seperti Erlang process registry, yang men-spawn thread dan melakukan register·lookup·unregister·kill terhadap ThreadId berdasarkan nama
    • Model sekuensial melacak thread id yang dibuat, pasangan name-thread yang terdaftar, dan thread id yang telah di-kill
    • Karena Register dan Unregister dapat gagal, response menggunakan Either ErrorCall ()
    • Informasi error location dari implementasi nyata dihapus dengan abstractError agar cocok dengan fake
    • monitoring menampilkan coverage RegisterFailed, RegisterSucceeded, UnregisterFailed, UnregisterSucceeded
    • Jika sengaja memasukkan bug yang membuat register menimpa 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 bentuk Fork [Register "b" (Var 0), Register "c" (Var 0)]
    • Masalahnya adalah race ketika thread lain dapat menyela di antara pemeriksaan dengan readRegistry dan pemanggilan atomicModifyIORef
    • Setelah menerapkan global lock pada register, unregister, dan kill, pengujian paralel lolos

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 IQueue memiliki iNew, iPut, iGet, iSize
    • Implementasi nyata langsung menghubungkan wrapper C queue
    • Implementasi fake menyimpan status model di IORef dan memperbaruinya melalui fNew, fPut, fGet, fSize
    • Komponen ditulis terhadap interface IQueue q
    • Dalam pengujian digunakan instance fake, sedangkan dalam deployment digunakan instance real
    • Dengan property-based test berbasis status, dibangun asumsi bahwa fake faithful terhadap real
  • Fake File System

    • Interface file system IFileSystem h memiliki iMkDir, iOpen, iWrite, iClose, iRead
    • Implementasi nyata menggunakan file system sungguhan di bawah /tmp/qc-test
    • Fake diimplementasikan sebagai FakeFS in-memory yang memiliki directory set, file content map, open handle map, dan next handle
    • fOpen, fWrite, fClose, fRead memodelkan 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
  • 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 IC
      • iB :: IC -> IO IB
      • iA :: 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

 
GN⁺ 2024-07-06
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...

    • Pembedaan antara pengujian berbasis properti dan fuzzing umumnya lebih berupa pengelompokan kasar berdasarkan nuansa
      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...
    • Fuzzing berbasis coverage dan pengujian berbasis properti sangat bisa digabungkan
      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
    • Karena sedang melakukan assertion atas properti, menurut definisi itu termasuk pengujian berbasis properti, seperti “semua node dengan level lebih dari 1 memiliki dua anak”
      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 tidak begitu tahu bagaimana fuzz test di Go berbeda dari isi artikel yang ditautkan, tetapi artikel itu mengatakan bahwa fuzzer yang layak harus dijalankan selama berhari-hari atau berminggu-minggu, dan bahwa pengujian berbasis properti hampir selalu sebaiknya dipilih dibanding fuzzing
      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?
    • Dari sudut pandang API, hal utama yang diperoleh adalah pustaka kombinator untuk membuat struktur data acak yang diinginkan
      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.alpha adalah pengalaman yang luar biasa, baik dipakai bersama test.check maupun tidak, tetapi ketika saya mencoba hypothesis di Python, hasilnya benar-benar buruk
    Hypothesis 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

    • Dalam kasus ini, kedengarannya bukan karena Hypothesis tidak bisa menangani kumpulan data besar, melainkan karena ia menolak banyak contoh yang sudah diperkecil
      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.alpha menangani hal ini secara berbeda
      Komentar 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”
    • Spec di Clojure terasa bagus karena sangat mudah membangun berbagai hal di sekelilingnya, tetapi setelah pindah ke Elixir, untuk menulis pengujian seperti itu saya harus turun sampai ke propEr, library Erlang lama. Cukup mengecewakan
    • Contoh di issue GitHub menggunakan filter dengan cara yang menimbulkan masalahnya sendiri
      Jika 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

    • Akan bagus jika sebagian penerbit menuntut reprodusibilitas, sementara sebagian lainnya tidak
      Setiap persyaratan punya efek mengecualikan, dan selalu ada kasus batas berupa makalah yang tetap bisa berguna meskipun tidak memenuhi persyaratan
    • Ini jelas bukan pertanyaan yang jawabannya tegas hitam-putih, bahkan bisa disebut pertanyaan politis, tetapi argumen pembelaan itu tetap tidak terlalu valid
      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
    • Mungkin bisa juga dengan membuka source code hanya kepada reviewer
      Sertakan juga semua yang diperlukan untuk menjalankan kodenya. Bisa jadi ini sudah dilakukan
    • Karena reprodusibilitas adalah landasan metode ilmiah
    • Makalah diterbitkan karena para penulis ingin menaikkan “indeks kepentingan” mereka, dan ini sangat langsung terkait dengan kompensasi serta peluang karier akademik
      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 proptest di Rust, dan biasanya cukup sederhana kalau dikodekan sendiri
    Contoh 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

    • Saya banyak menulis property test manual di Rust, dan umumnya bentuknya seperti ini
      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 StateModel pada dasarnya juga meminta hal yang sama. Sulit melihat StateModel benar-benar memberi banyak nilai; tampaknya hanya menghilangkan sangat sedikit kode test dalam pengalaman nyata, dengan imbalan menambahkan jauh lebih banyak kode framework yang harus dipahami

    • Ada test yang penilaian seperti itu tepat, tetapi bagian shrinking kasus gagal sering kali rumit
      Jika 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 proptest yang stateful mungkin berlebihan. Namun jika harus mempertahankan state model dan menetapkan prasyarat untuk berbagai operasi, framework khusus banyak meringankan pekerjaan
      Tahun 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
    • Menurut saya bagian stateful lebih baik ditangani oleh model-based testing
      Gaya testing boleh dicampur. Toh itu kode sendiri
    • Pemahaman saya, QuickCheck paralel memastikan bahwa semua interleaving yang mungkin dalam program multithreaded pada akhirnya menghasilkan state yang juga dapat dicapai jika perintah dipanggil secara sekuensial
      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

    • Bagian yang mengatakan bahwa “integrated shrinking” dengan rose tree (misalnya Hedgehog) mengikuti constraint generator tetapi bermasalah dengan monadic bind, sejauh batas pengetahuan amatir saya, tampaknya merupakan keterbatasan mendasar dari monadic bind/generator
      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
    • Secara pribadi, Hypothesis bagi saya jauh dari “langsung jalan”
      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.alpha dengan maupun tanpa test.check, jadi meskipun ada perbedaan, saya tidak sepenuhnya asing dengan ide umumnya
      [0] https://github.com/HypothesisWorks/hypothesis/issues/3493
    • Framework yang saya sukai adalah falsify, dan menyediakan “internal integrated shrinking”
      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

    • Saya penasaran jenis properti apa yang Anda maksud
      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
    • Jika memungkinkan, jelas lebih baik memaksakan constraint pada waktu kompilasi
      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