Property-Based test kütüphanelerinin iç karartıcı durumu
(stevana.github.io)- Property-based testing, QuickCheck’ten sonra birçok dile yayıldı, ancak Temmuz 2024 itibarıyla birçok kütüphane 2009’da zaten tanımlanmış olan durum tabanlı test ve paralel test özelliklerini yeterince sunamıyor
- Temel boşluk, durum makinesi modeliyle sıralı durum değişimlerini doğrulama ve aynı modeli doğrusallaştırılabilirlik (linearisability) kontrolünde yeniden kullanarak paralel yürütmede race condition’ları bulma yeteneğinde yatıyor
- İncelenenlerin çoğunda durum tabanlı test ya yok ya da deneysel; paralel test ise daha da nadir ve FsCheck, Gopter, RapidCheck, SwiftCheck, jsverify gibi projelerde ilgili issue’lar yıllardır açık duruyor
- Yaklaşık 400 satırlık bir Haskell implementasyonu, durum tabanlı ve paralel property-based testing’i yeniden üretirken, geleneksel durum makinesi spesifikasyonu yerine programcıların aşina olduğu fake tabanlı referans implementasyonu modeli olarak kullanıyor
- Sözleşme testinden geçirilmiş fake’ler, tek bir bileşeni doğrulamanın ötesinde, gerçek bağımlılıkların yerine enjekte edilen hızlı ve deterministik entegrasyon testlerinde de yeniden kullanılabiliyor
QuickCheck sonrası oluşan özellik açığı
- Property-based testing, “test yazma, üret” sloganıyla birçok programlama dili topluluğuna yayıldı
- Asıl Haskell kütüphanesi olan QuickCheck’in Wikipedia sayfasında diğer diller için 57 yeniden implementasyon listeleniyor
- İlk QuickCheck makalesi olan QuickCheck: A Lightweight Tool for Random Testing of Haskell Programs, ICFP 2000’de yayımlandı ve ilk implementasyonun tüm kaynak kodu makale ekindeki yaklaşık 300 satırlık koddan oluşuyordu
- İlk QuickCheck yalnızca saf fonksiyonları test edebiliyordu; 2002 tarihli Testing monadic code with QuickCheck ise mutable state, dosya I/O’su ve ağ iletişimi gibi etkili kodları ele almak için temel oluşturdu
Durum tabanlı ve paralel testin ortaya çıkışı
- Quviq AB, 2006’da John Hughes ve Thomas Arts tarafından kuruldu; ilk kullanım örneklerinden biri Ericsson’un Erlang projesinin test edilmesiydi
- Erlang saf işlevsel bir dil değil ve eşzamanlılık yaygın olduğundan, mevcut monadic QuickCheck tek başına yeterince kullanışlı değildi
- Quviq’in closed source Erlang QuickCheck’i, sonradan birçok açık kaynak implementasyonda eksik kalan iki özelliği içeriyordu
- Durum makinesi modeli kullanan sıralı durum tabanlı property-based testing
- Aynı sıralı durum makinesi modelini yeniden kullanarak race condition tespit eden paralel test
- Durum tabanlı test, QuickCheck testing for fun and profit (2007) çalışmasında bugünkü biçimiyle ortaya çıkıyor
- Paralel test, Finding Race Conditions in Erlang with QuickCheck and PULSE (ICFP 2009) makalesinde ayrıntılı biçimde ele alınıyor ve temel teknik olarak Herlihy ile Wing’in Linearizability: a correctness condition for concurrent objects (1990) çalışmasını kullanıyor
- Quviq QuickCheck’in kütüphane kodu makalelerle paylaşılmadı; kamuya açık olanlar API ve bu API’yi kullanan test örnekleriydi
2024 kütüphane incelemesinin sonuçları
- Güncel state-of-the-art, durum makinesi modeli tabanlı stateful testing ve aynı sıralı modeli linearisability ile birleştiren parallel testing olarak görülüyor
- İnceleme, Temmuz 2024 itibarıyla dokümantasyon, issue tracker’lar ve bazı kaynak kodlarının okunup derlenmesine dayanıyor
- Birçok kütüphane durum tabanlı testi hiç sunmuyor ya da sınırlı sunuyor
- QuickCheck (Haskell) için durum tabanlı test ekleme issue’su 2016’dan beri açık
- SwiftCheck için de durum tabanlı test ekleme issue’su 2016’dan beri açık
- jsverify’de durum tabanlı test ekleme issue’su 2015’ten beri duruyor
- proptest (Rust) için ayrı bir proptest-state-machine projesine bakmak gerekiyor
- Paralel test desteği daha da nadir
- Gopter’in README’sinde “No parallel commands … yet?” yazıyor ve 2017 tarihli bir issue bulunuyor
- FsCheck’te parallel support ekleme issue’su 2016’dan beri açık
- RapidCheck’te parallel support ekleme issue’su 2015’ten beri açık
- propcheck’te 2020’den beri parallel testing ekleme issue’su var
- Her iki özelliği de destekleyen açık kaynak örnekleri olarak PropEr, Hedgehog, qcheck-stm, quickcheck-state-machine, stateful-check gösteriliyor
- Paralel özellik olsa bile kısıtlı kalan örnekler de var
- QuickTheories’in paralel testinde, olası end state sayısı komut sayısına göre hızla arttığı için command list’in genelde 10’un altında tutulması gerektiği kaynak kod yorumlarında belirtiliyor
- ScalaCheck’in LevelDB ve Redis örnekleri,
threadCount = 1olan sıralı örnekler olarak sunuluyor - fast-check’in race condition desteği, Quviq QuickCheck’in paralel testinden farklı olarak sıralı durum makinesi modelinin yeniden kullanımı ya da linearisability kullanımı gibi görünmüyor
- Paralel testin sonradan eklendiği net örnekler görünmüyor; API tasarımına en baştan dahil edilmezse ciddi bir yeniden tasarım gerekebilir
Özelliklerin yayılmasının yavaş olmasının nedenleri
- John Hughes’un sunduğu üç neden var
- Durum tabanlı ve paralel test, saf fonksiyon testleri kadar faydalı değil
- Durum makinesi modeli yazmak, sıradan testlerden farklı bir düşünme biçimi ve eğitim gerektiriyor
- Yalnızca open source ile endüstriyel benimseme iyi gitmedi; closed source ürün, eğitim ve danışmanlık benimsemeyi destekledi
- Yalnızca saf fonksiyon parçalarına property-based testing uygulamak bile büyük fayda sağlayabilir, ancak endüstriyel sistemlerde database, stateful protocol ve concurrent data structure çok yaygın olduğundan durum tabanlı ve paralel test de neredeyse aynı derecede önemli
- Durum tabanlı spesifikasyonlar her zaman saf fonksiyon spesifikasyonlarından daha zor olmak zorunda değil
- Bir key-value store modeli, yalnızca key-value pair listesinden bile oldukça ileri gidebilir
- LevelDB örneğinde basit bir model, birkaç dakika içinde 17 adımlık küçültülmüş bir counterexample buldu; Google düzeltmesinden sonra da yine birkaç dakika içinde 31 adımlık bir counterexample bulundu
- İkinci sorun, background compaction process içindeki bir bug’dı; compaction, okuma performansını artırmak ve disk alanını geri kazanmak için önemliydi, ancak modele açıkça dahil edilmemişti
- Closed source, endüstriyel benimsemeye yardımcı olmuş olabilir, ancak open source benimsemesine yardımcı olmadığı değerlendiriliyor
- Makalelerdeki sonuçları Quviq QuickCheck lisansı olmadan yeniden üretmek için çok fazla reverse engineering gerektiği ve bunun neredeyse imkânsız olduğu düşünülüyor
Öneri: küçük ve açık bir implementasyon ile daha kolay spesifikasyon
- İyileştirme yönü iki başlıkta toplanıyor
- İlk QuickCheck’in yaklaşık 300 satırlık implementasyonu gibi, durum tabanlı ve paralel property-based testing için kısa bir açık kaynak implementasyon sunmak
- Durum makinesi yerine, programcıların zaten aşina olduğu mock ve test double kavramlarını yeniden kullanarak spesifikasyon yazma yükünü azaltmak
- Bu hipotezi doğrulamak için iki nokta gösteriliyor
- Durum tabanlı ve paralel property-based testing yaklaşık 400 satır kodla implementasyon ediliyor
- Durum makinesi yerine, bellek içi referans implementasyonu yani bir fake, model olarak kullanılıyor
Saf property-based testing özeti
- Saf fonksiyon testlerinde girdiler üretilir ve fonksiyon çıktısının girdiyle hangi ilişkiyi sağladığı doğrulanır
- Örneğin
reverse, rastgele birxslistesi içinreverse (reverse xs) == xsözelliğiyle test edilebilir - QuickCheck varsayılan olarak 100 test üretir; başarısızlık durumunda girdiyi shrink ederek en küçük counterexample'ı sunar
reverse xs == xsgibi hatalı bir property,[0,1]gibi en küçük karşı örneğe indirgenir- Sık görülen property kalıpları arasında inverse, idempotency, associativity, abstract data type aksiyomları, metamorphic property gibi örnekler bulunur
- inverse:
deserialise (serialise i) == i - idempotency:
sort (sort xs) == sort xs - associativity:
(i + j) + k == i + (j + k)
- inverse:
Durum tabanlı property-based testing
- Durum içeren bileşenlerde aynı girdi her zaman aynı çıktıyı üretmez
- Bir sayacın ilk
incrsonucuyla ikinciincrsonucu önceki duruma bağlı olarak değişir - Database ve file system de önceki girdi geçmişinin sonraki çıktıyı etkilemesine neden olur
- Bir sayacın ilk
- Saf fonksiyon testleri tek bir girdiyi ele alırken, durum tabanlı testler girdi dizileri üreterek sistemin zaman içinde nasıl değiştiğini doğrular
- Model,
m -> i -> (m, o)biçimindeki bir fake ile ifade edilir- Önceki model durumu
mve girdiiüzerinden sonraki model ve çıktıohesaplanır - Gerçek sistemin çıktısı ile fake'in çıktısı her adımda karşılaştırılır
- Uyumsuzluk varsa girdi dizisi shrink edilerek küçük bir counterexample bulunur
- Önceki model durumu
-
Counter örneği
- Global mutable variable kullanan bir Haskell counter test hedefi olarak alınır
incrsayacı artırır,getise mevcut değeri okur- Model için tek bir
Counter Intyeterlidir veStateModelinstance'ıCounter 0,Incr,Get,Incr_ (),Get_ Int,runFake,runReal, command generator tanımlar incr42Buggibi, sayaç değeri 42 olduğunda artırmayan bir bug eklendiğinde QuickCheck 66 testten sonra hatayı bulur, 29 kez shrink ederek 43 increment sonrasıGetçağrısı yapan en küçük karşı örneği sunar- Testler arasında gerçek global counter
resetedilmezse model her zaman 0'dan başlarken gerçek counter önceki test durumunu korur ve mismatch oluşur
-
Durum tabanlı kütüphane arayüzü
StateModelarayüzü test edilen sistemi bir black box olarak görür; command girdiyi, response ise çıktıyı temsil eder- Temel bileşenler
Command state,Response state,initialState,runFake,runReal,generateCommand'dır - İsteğe bağlı bileşenler şunlardır
Reference: file handle gibi, önceki response'un oluşturduğu bir kaynağın sonraki command'da referans alınması gerektiğinde kullanılırPreconditionFailure: örneğin açık olmayan bir file handle üzerindenreadyapılamamasını ifade eden precondition hatasını temsil ederCommandMonad: varsayılanIO'dur ama başka monad'lar da kullanılabilirmonitoring,commandName: coverage ve istatistikler için kullanılır
- Command üretilirken gerçek file handle gibi değerler oluşturulamayacağı için
Var Intbiçiminde symbolic reference üretilir ve çalışma sırasında gerçek reference'a dönüştürülür - Shrink sonrasında, precondition'ı bozan veya scope dışındaki symbolic reference'ları kullanan command'lar kaldırılır
-
Circular buffer örneği
- C ile yazılmış circular queue, Haskell FFI üzerinden test edilir; model ise basit bir liste tabanlı queue olarak yazılır
- C implementasyonu error checking yapmadığı için boş queue üzerinde
getçağrısı yapıldığında initialize edilmemiş memory döndürebilir - Gerçek implementasyon circular index sayesinde verimlidir ama açıkça correct olduğu söylenemez; fake ise daha az verimlidir fakat test için bu sorun değildir
newbir queue reference döndürdüğü için model, birden çok queue'yu yönetmek üzereMap (Var Queue) FQueuekullanır- Başta dolu queue'ya
putyapmayı engelleyen precondition eksiktir; bu yüzden boyutu 1 olan bir queue'ya0,1konup ardındangetçağrıldığında model FIFO olduğu için0beklerken C kodu1döndürür - Bunun nedeni bir implementasyon bug'ı değil, modeldeki precondition eksikliğidir;
QueueIsFullprecondition'ı eklenerek düzeltilir Sizecommand'ının generator'da yer almadığı, coverage çıktısında ortaya çıkar; eklendikten sonra queue size hesaplama bug'ı bulunur- Boyutu 1 olan bir queue'ya bir öğe eklenip
Sizeçağrıldığında beklenen değer 1 iken gerçek değer 0'dır; çözüm olaraknewiçinde dahili buffer boyutununn + 1yapılması önerilir - Sonrasında
abs(q->inp - q->outp) % q->sizeifadesi boyut 1 için geçse de boyut 2'de yine başarısız olur; nihai düzeltme(q->inp - q->outp + q->size) % q->sizeolur
-
Die Hard 3 su bidonu bulmacası
- 3L ve 5L bidonlarla tam 4L elde etme bulmacası, durum tabanlı testle çözülür
- Gerçek bir implementasyon olmadan yalnızca model ve fake çalıştırılsa bile belirli bir duruma ulaşıldığında test başarısız kılınarak shrink edilmiş action sequence elde edilebilir
- 199 test ve 11 shrink sonrasında sunulan sequence şu akışı izler
- 5L bidonu doldur
- 5L'den 3L'ye dök
- 3L bidonunu boşalt
- 5L'den 3L'ye tekrar dök
- 5L bidonunu doldur
- 5L'den 3L'ye dök
- Trace içinde ara durumlar gösterilir ve böylece büyük bidonun 4L olduğu süreç doğrulanabilir
Paralel property-based testing
- concurrent code hatalarını yeniden üretmek ve düzeltmeyi doğrulamak zordur; çünkü thread interleaving her çalıştırmada farklıdır
- amaç, kullanıcının çok fazla ek test kodu yazmadan, sıralı durum tabanlı testlerde olduğu gibi paralel test yapabilmesini sağlamaktır
- counter örneğinde
incr,readIORefsonrasıwriteIORefişlemini atomik olmayan biçimde yaparsa, iki thread birbirinin increment'ini ezerek race condition oluşturabilir - paralel test, çalışma sırasında command invocation ve response anlarını toplayıp bir concurrent history oluşturur ve bu history'nin hangi sıralı interleaving ile açıklanabildiğini denetler
- herhangi bir interleaving sıralı modelle uyuşuyorsa, history'nin linearise olduğu kabul edilir ve doğru sayılır
- hiçbir sıralı interleaving gerçek response'u açıklayamıyorsa, sonuç non-linearisable olarak değerlendirilir
-
Paralel command üretimi ve shrink
- paralel program,
ParallelCommandsve birden çokForkile ifade edilir; herForkiçindeki command'ler paralel çalıştırılır - örnek uygulama single, double ve triple threaded execution'ı ele alır
- paralel çalıştırmada
Fork [Write "a" "foo", Write "a" "bar"]gibi durumlarda, interleaving'e göre mümkün model durumları değişebilir - paralel model, tek bir state yerine state kümesi temelinde command üretimi ve shrink yapar
parallelSafe,Forkiçindeki command'lerin tüm permutation'larında precondition'ın korunup korunmadığını kontrol eder- örneğin
Write "a"ileDelete "a"aynı fork içinde olursa, bir command diğer command'in precondition'ını bozabilir - shrink sürecinde de yalnızca precondition'ı ve symbolic reference scope'u koruyan command'ler bırakılır
- paralel program,
-
Paralel çalıştırma ve linearisability denetimi
- paralel çalıştırma, her command'in
InvokeveOkevent'ini history olarak kaydeder - response yeni bir reference içeriyorsa, thread'ler arasında reference numarası çakışmasını önlemek için ortam atomik bir sayaçla genişletilir
- history'deki tüm olası interleaving'ler
Rosetree olarak listelenir linearisable, bu tree'deki herhangi bir path'in sıralırunFakemodeli ve response ile eşleşip eşleşmediğini kontrol eder- paralel test sonuçta sıralı modeli yeniden kullandığından, kullanıcı sıralı modeli yazdıktan sonra az ek kodla paralel test elde eder
- paralel çalıştırma, her command'in
-
Parallel counter örneği
- counter için paralel testi etkinleştirmek amacıyla eklenen kod yalnızca
ParallelModel Counterinstance'ı ve property'dir - atomik olmayan
incrRaceConditionkullanılırsa, race condition bulunur - daha küçük bir test case'te de race olsa bile, farklı bir interleaving yüzünden hata yeniden üretilemezse QuickCheck daha küçük test case'in geçtiğini varsayıp shrink'i durdurabilir
- doğru çözüm deterministic bir thread scheduler'dır ve paralel test makalesi bunu kullanır
- örnek uygulama daha basit bir workaround olarak, aynı interleaving'in oluşma olasılığını artırmak için shared memory read/write çevresine kısa sleep'ler ekler
- sleep, race bulmak için değil, bulunan race'in counterexample'ını küçültmek için gereklidir
- sleep eklendikten sonra en küçük karşı örnek
ParallelCommands [Fork [Incr,Incr],Fork [Get]]biçimine küçülür
- counter için paralel testi etkinleştirmek amacıyla eklenen kod yalnızca
-
Process registry örneği
- Erlang process registry gibi, thread spawn eden ve
ThreadIddeğerlerini isimleregister·lookup·unregister·killeden bir sistem örnek olarak kullanılır - sıralı model, oluşturulan thread id'leri, kaydedilen name-thread çiftlerini ve kill edilen thread id'lerini izler
RegisterveUnregisterbaşarısız olabileceği için response'taEither ErrorCall ()kullanılır- gerçek uygulamadaki error location bilgisi, fake ile eşleştirmek için
abstractErrorile kaldırılır monitoring,RegisterFailed,RegisterSucceeded,UnregisterFailed,UnregisterSucceededcoverage'ını gösterir- bilerek
registerın mevcut registry'yi ezdiği bir hata eklenirse, daha önce kaydedilmiş"e"için unregister yapılamayan sıralı bir karşı örnek ortaya çıkar - paralel testte daha uzun bir karşı örnek çıkar ve
SleepyIORefkullanıldığındaFork [Register "b" (Var 0), Register "c" (Var 0)]biçimine shrink edilir - sorun,
readRegistryile kontrol yapıldıktan sonraatomicModifyIORefçağrılana kadar başka bir thread'in araya girebilmesinden kaynaklanan bir race'tir register,unregistervekilliçin global lock uygulandıktan sonra paralel test geçer
- Erlang process registry gibi, thread spawn eden ve
Fake tabanlı modeller ve entegrasyon testleri
- Geleneksel state machine specification with post-conditions yerine reference implementation olarak bellek içi fake kullanılır
- Edsko de Vries’in 2019 tarihli yazısı, fake’i post-condition tabanlı durum makinesi belirtiminin üzerine kurma yaklaşımını öneren ilk yazı olarak tanıtılır
- fake, mock’a benzer ama formal specification’a alışık olmayan programcılar için daha kolay bir yaklaşım olarak sunulur
- fake’in, entegrasyon testlerinde bağımlı bileşenlerin yerine kullanılabilmesi gibi bir avantajı da vardır
- Gerçek dependency’yi başlatmak veya etkinleştirmek gerekmez
- Daha hızlı ve deterministic entegrasyon testleri kurulabilir
- fake’in hatalı olabilmesi sorunu contract test ile ele alınır
- Durum tabanlı ve paralel property-based testler, fake ile gerçek implementasyonun uyuştuğunu doğruladığı için fake, sözleşme testi yapılmış bir bağımlılık işlevi görür
-
Queue fake ile test ve dağıtımı ayırma
- queue arayüzü
IQueue,iNew,iPut,iGet,iSizeiçerir - Gerçek implementasyon, C queue wrapper’ını doğrudan bağlar
- fake implementasyon, model durumunu
IORefiçinde saklar vefNew,fPut,fGet,fSizeüzerinden günceller - Bileşen,
IQueue qarayüzüne karşı yazılır - Testlerde
fakeinstance’ı, dağıtımda iserealinstance’ı kullanılır - Durum tabanlı property-based test ile fake’in real’e faithful olduğu varsayılır
- queue arayüzü
-
File system fake
- file system arayüzü
IFileSystem h,iMkDir,iOpen,iWrite,iClose,iReadiçerir - Gerçek implementasyon,
/tmp/qc-testaltındaki gerçek file system’i kullanır - fake, directory set, file content map, open handle map ve next handle içeren bellek içi
FakeFSolarak uygulanır fOpen,fWrite,fClose,fRead, meşgul dosya, var olmayan directory ve kapatılmış handle gibi precondition failure durumlarını modeller- fake file system’in gerçek file system’e faithful olduğu test edilirse, file system’e bağımlı bileşenler fake ile entegrasyon testine tabi tutulabilir ve dağıtım sırasında gerçek file system ile değiştirilebilir
- real ile değiştirildiğinde hata çıkarsa, fake ile real arasındaki mismatch’in durum tabanlı property-based testleri nasıl geçtiği araştırılmalıdır
- file system arayüzü
-
Daha büyük bileşen sistemleri
- A’nın B’ye, B’nin de C’ye bağımlı olduğu sistemler de aynı şekilde genişletilebilir
- Her bileşen için bir arayüz tanımlanır
iC :: IO ICiB :: IC -> IO IBiA :: IB -> IO IA
- Test stratejisi şöyledir
- C, durum tabanlı ve paralel property-based testlerle doğrulanarak contract-tested fake C elde edilir
- B’nin integration testlerinde fake C kullanılır
- A testlerinde, fake C kullanan fake B kullanılır
- Bu yaklaşım, daha fazla bileşen veya servise de aynı desenle genişletilebilir
Sonuç
- Durum tabanlı ve paralel property-based testing yaklaşık 400 satır kodla uygulanabilir; bu da shrinking olmayan ilk QuickCheck’in yaklaşık 300 satırlık implementasyonuyla karşılaştırılabilecek bir ölçektir
- fake’i model olarak kullanmak, durum tabanlı ve paralel testlerin belirtimini daha tanıdık bir biçime sokar ve daha büyük sistemleri compositional şekilde test etmek için yeniden kullanılabilir
- Her dil topluluğu denemeyi sürdürürse property-based testing kütüphanelerinin durumunu iyileştirmek için alan vardır
1 yorum
Hacker News yorumları
Kapsama dayalı fuzzing ortaya çıktı ve Go’da da iyi destekleniyor; property-based test kütüphanesi kullanmazsak neyi kaçırdığımızı merak ediyorum
https://www.tedinski.com/2018/12/11/fuzzing-and-property-tes...
Aşağıdaki fuzz testine ve ona karşılık gelen değişmez koşul kontrolüne bakınca, aslında property test ile neredeyse aynı şey değil mi diye düşünüyorum
https://github.com/ncruces/aa/blob/505cbbf94973042cc7af4d6be...
https://github.com/ncruces/aa/blob/505cbbf94973042cc7af4d6be...
Gerçek farklar var ama sınırlar epey bulanık; neyin fuzzing, neyin property-based test olduğunu kesin biçimde ayırmak çok da önemli değil
Hızlı çalışan testler ve ayrıntılı assertion’lar property-based test tarafına, uzun süre çalışıp yalnızca çökme arayanlar fuzzing tarafına girer; arası ise belirsiz
https://hypothesis.works/articles/what-is-property-based-tes...
Google’dayken ikisini birleştiren dahili bir araç gerçekten iyiydi. Her zamanki gibi property-based test yazıyordunuz; çalıştırma sırasında test framework’ü kapsama bilgisi elde edecek şekilde özel olarak derliyor ve kapsama oranını artırmak için rastgele girdileri ayarlıyordu. Elbette birden fazla makineden oluşan bir cluster üzerinde tamamen otomatik çalışıyordu
Geleneksel property-based test genellikle yalnızca kütüphane olarak uygulanır; bu yüzden rastgele girdi üretimini yönlendirecek kapsama bilgisi her zaman bulunmayabilir
Ancak kütüphaneye bağlı olarak epey kullanışlı özellikler elde edebilirsiniz. Güzel özelliklerden biri shrinking’dir; buradaki “Shrinking” bölümüne bakabilirsiniz: https://tech.fpcomplete.com/blog/quickcheck-hedgehog-validit...
Generator’ları birleştiren combinator’lar da harikadır; kütüphaneye bağlı olarak istisnai davranışlara yol açtığı bilinen “kötü” değer kümeleri de bulunabilir
Bir adım geri çekilip test hakkında daha meta bir soru sormak istiyorum. Test başarısı, kodun başarılı olduğu anlamına gelir mi ve bunun tersi de geçerli mi? Go’nun sözleşmesinde aynı girdiyi aynı koda verdiğinizde aynı çıktının alınacağına dair açık bir bölüm var mı?
Rastgele nesneler kümesini temsil eden Arbitrary tipiyle çalışırsanız, test girdileri üreten yeniden kullanılabilir fonksiyonları kolayca yazabilirsiniz. Böyle bir kütüphane Go’nun fuzzing framework’üyle de oldukça kolay birlikte kullanılabilir gibi görünüyor
Yine de map, filter, chain, oneOf gibi yaygın combinator’ların biraz hantal olabileceğini düşündüğüm için JavaScript için yeni bir property test kütüphanesi yazıyorum. Amacım kullanımını daha iyi hale getirmek; ama hâlâ deneysel ve henüz yayımlanmadı
clojure.spec.alpha,test.checkile birlikte kullanılsa da kullanılmasa da harika bir deneyimdi; Python’dakihypothesisi deneyince gerçekten berbat buldumHypothesis, basit ama “büyük” veri kümelerini tasarım gereği işleyemiyor gibi görünüyordu; buradaki “büyük” de aslında o kadar büyük değil. [0] O kadar acı vericiydi ki iş yerindeki Python test takımından Hypothesis’i ve üretim tabanlı testleri tamamen söküp attım
[0] https://github.com/HypothesisWorks/hypothesis/issues/3493
Hypothesis, hatanın 0’da da var olup olmadığını görmek için üretilen tamsayıyı 0’a küçültmeye çalışmış; test ise 0 içerdiği için bunu başarısızlık değil, ret olarak değerlendirmiş. Küçük örneklerde bu yalnızca verimsizlikti, ama büyük örneklerde Hypothesis’in pes edeceği noktaya gelmiş
O başlıkta biri, 0 üretemeyen başka bir örnek üretme stratejisi kullanılmasını önermişti. Yani Hypothesis’in küçültücüsünün en sevdiği değeri üretip sonra reddetmek yerine, en baştan hiç üretmemek. Bunu denemiş mi merak ediyorum
clojure.spec.alphanın bunu nasıl farklı ele aldığını da merak ediyorumhttps://news.ycombinator.com/item?id=40876437 adresindeki mjaniczek yorumunda bu örnek, Hypothesis yaklaşımının bir dezavantajı olarak gösteriliyor
“Üreteç artık başarısız olabilen bir bayt listesi ayrıştırıcısına dönüştüğü için biraz verimsizlik oluşur ve kullanıcı, iç küçültücünün kusursuz biçimde küçültemeyeceği tuhaf bir üreteç oluşturabilir. Yine de üç yaklaşım içinde geliştirici deneyimi en iyisi…” anlamında
Elbette kişi kendi testini “tuhaf” bir şekilde yazdığı konusunda muhtemelen hemfikir olmayacaktır
filterı kendi sorununu yaratacak şekilde kullanıyorRastgele üretip sonra belirli bir özelliğe uyanları süzerseniz, üretim sürecinde fiilen piyango bileti kazımış olursunuz
Yazıda sorulan “Yayımlanmış araştırmaların açık kaynak araçlarla ya da en azından kamuya ve diğer araştırmacılara ücretsiz sunulan araçlarla yeniden üretilebilir olmasını neden şart koşmuyoruz?” sorusunun basit cevabı, böyle bir şartın doğrudan sonucunun bu koşulu karşılamayan makalelerin yayımlanmaması olacağıdır
Örneğin yazarlar ve başkaları için faydalı olmuş gibi görünen Quviq QuickCheck makalesi gibi bir çalışma da yayımlanmayacaktı ve topluluk bu bilgi armağanını kaybedecekti
Her şartın dışlayıcı bir etkisi vardır ve şartları karşılamasa da faydalı olabilecek makaleler gibi sınır durumlar her zaman bulunur
Bu mantığı geçerli sayarsak, onu kalkan yapıp istediğimiz yere kadar gidebiliriz. Yeniden üretilebilirliği şartlardan çıkarırsak, açıklamak istemediğimiz hiçbir şeyi açıklamak zorunda kalmayız. Örnekleme ilişkin verileri de, istatistiksel anlamlılık testlerini de sunmak gerekmez. Belirli bir sonucu elde ettiğini iddia eden muğlak bir özet yeterli hale gelir
Hatta Fermat’nın kendi Arithmetica nüshasının kenarına düştüğü ünlü not bile tamamen geçerli bir araştırma makalesi olur. Ne de olsa ünlü bir matematikçinin bir teorem için kısa ve zarif bir kanıta sahip olduğunu düşündüğü yönündeki değerli bilgiyi kaybetmek istemeyiz. Tabii gerçekte büyük olasılıkla böyle bir kanıt yoktu
Bu siyasi soru hakkındaki görüşüm, mevcut standartların fazla gevşek olduğu yönünde. Kimse bir şeyi yayımlamaya zorlanmıyor. Dünyada, mülkiyet değeri gibi nedenlerle hiçbir yerde yayımlanmayan çok sayıda araştırma var ve bu araştırmalar ortadan kaybolmayacak
Ama akademide çalışıyor, üstelik araştırma fonu da alıyor ve amacınızın dünyanın bilimsel bilgisini ilerletmek olduğunu söylüyorsanız, gerçekten o amacı izlemenizin istenmesi adildir. Sırf akademik kariyer merdivenini tırmanmak için o amacı izliyormuş gibi yapmamalısınız
Kodu çalıştırmak için gerekenleri de birlikte sağlamak yeterli. Belki zaten böyle yapıyorlardır
Bu amaç uğruna gereksinimler artsa bile yayımlanan makale sayısının azalması pek olası değil
Yayımlanan makalelerdeki daha ciddi sorun, mümkün olduğunca çok ve hızlı yayımlamak için hataların sık sık bilerek görmezden gelinmesidir. Makaleleri doğrulamak kolaylaşırsa bu durumun iyileşme ihtimali var, ama çok da umutlanmazdım. İnsanlar kestirme yollar bulmakta çok beceriklidir
Rust'ın
proptestile durum içeren property testleri oldukça sık yazıyorum; genelde doğrudan kodlamak epey basit oluyor6 hata bulan, önemsiz olmayan bir örnek https://github.com/sunshowers-code/buf-list/blob/main/src/cu... adresinde var
Paralel testler zaman zaman yararlı olabilir, ancak çoğu durumda sadece çok sayıda testi paralel çalıştırmak daha kolay oluyor
En üst düzeyde gerçek rastgelelik kullanıyorum; altında da düşük karmaşıklıktaki örneklerden yüksek karmaşıklıktaki örneklere çıkan birkaç iç içe döngü oluyor. Ardından deterministik sözde rastgele sayı üretecine verilecek seed'i oluşturup çıktıya yazıyorum. Test başarısız olursa, hata seed'ini kopyalayıp yapıştırarak başarısız vakayı yeniden üretebiliyorum
Bu tür manuel property testlerinin herhangi bir framework veya kütüphaneden daha hızlı, daha esnek ve genel olarak daha az zahmetli olduğunu hissettim
Ancak gerçekten sağlam eşzamanlılık testleri için AWS Shuttle kütüphanesini (https://github.com/awslabs/shuttle) şiddetle tavsiye ederim. İnanması zor derecede karmaşık yarış durumlarını bulabiliyor. Küçük bir öğretici de yazdım: https://grantslatton.com/shuttle
AWS'te bu kütüphane, AWS S3'ü çalıştırmak için yazdığımız özel dosya sistemini doğrulamakta kullanıldı
Bağlantı verilen “Testing Telecoms Software with Quviq QuickCheck” makalesine hızlıca göz attım, ancak “bu durum içeren işi neden doğrudan kendimiz yapmak daha iyi olmasın?” sorusunun yanıtı hemen görünmüyordu
Metin bu kısım için bir anahtar-değer deposunun anahtar-değer çifti modelini gösteriyor, ama neden sadece bir durum makinesi yazmayalım, neden bir framework gerekli, anlamıyorum. Geçen hafta işte dosya sistemi etkileşimlerini test etmek için kelimenin tam anlamıyla bunu yaptık ve sonunda
type Instruction = | Read of stuff | Write of stuff | Seek of stuff | …gibi bir şeye vardıBöylece property “bu komut listesi verildiğinde, …” oluyor.
StateModeltürü de temelde aynı şeyi istiyor.StateModelin üzerine düşeni yaptığına ikna olmak zor; anlaşılması gereken çok daha fazla framework kodu eklemek karşılığında, gerçek deneyimde çok az miktarda test kodunu ortadan kaldırıyormuş gibi görünüyorYalnızca “geçerli” durum geçiş dizileri üretmek istiyorsanız, belirli bir durumda hangi test adımlarının geçerli olduğunu belirleyen bir model durumuna genellikle ihtiyaç duyarsınız. Ayrıca küçültme sırasında test adımlarını kaldırırken, başlangıçta her adımı üretirken uyulan önkoşulları bozup sahte başarısızlıklara yol açmamaya da dikkat etmeniz gerekir
Her durumda her işlemin geçerli olduğu ve tamamen rastgele keyfi işlem dizilerinden başka bir şey istemiyorsanız, durum içeren
proptestframework'ü fazla gelebilir. Ancak model durumunu korumanız ve çeşitli işlemlerin önkoşullarını belirtmeniz gerekiyorsa, özel bir framework birçok işi üzerinizden alırGeçen yıl bu konuda bir blog yazısı yazmıştım; daha derin bir örnek merak ediyorsanız bakmaya değer: https://readyset.io/blog/stateful-property-testing-in-rust
Başkalarının da söylediği gibi, paralel durum makinesi testi de özel bir framework'ten elde edebileceğiniz güzel avantajlardan biri; ama tek avantajı değil
Test stillerini karıştırıp kullanabilirsiniz. Kod sizin kodunuz
Avantajı bu
Yazar, özellik tabanlı testin durum makinesi ve paralel yönlerine odaklanıyor, ancak daha büyük etki yaratabilecek başka yönler de var
Bunlardan biri kapsam yönlendirmeli özellik tabanlı test; Dan Luu’nun yazısına bakılabilir: https://danluu.com/testing/
Bir diğeri de benim taraflı olduğum konu: değer üretirken oluşturulan tüm değişmez koşulları koruyarak küçültmeyi otomatikleştirmek
Özetle, değerler üzerinde çalışan QuickCheck tarzı türetilmiş küçültme fonksiyonlarının (
shrink : a -> [a]) kısıtları ve sorunları var; bu yüzden insanlar sorunla uğraşmak yerine küçültmeyi kapatıyorRose tree “entegre küçültme” (örn. Hedgehog), üretecin kısıtlarını izler; ancak monad bind, yani üreteç sonucunu kullanarak başka bir üretece dallanma durumunda sorun yaşar
Sihirli biçimde “kendiliğinden çalışıyor” gibi görünen tek yaklaşım Hypothesis’in iç küçültmesi. Değerin kendisini değil, rastgele seçimler listesini küçülten dolaylı bir katman kullanır. Dezavantajı, üretecin artık başarısız olabilen bir bayt listesi ayrıştırıcısına dönüşmesi ve biraz verimsizlik yaratması; ayrıca kullanıcıların iç küçültücünün kusursuz biçimde küçültemediği tuhaf üreteçler yazabilmesidir. Yine de üç yaklaşım içinde geliştirici deneyimi en iyi olan bu; insanların test yazmasının bile küçük bir mucize olduğunu düşününce, bir test kütüphanesi yazarı olarak inşa etmeye en değer yaklaşım gibi geliyor
Bunun yerine, en iyi küçültme için applicative üreteçler tercih edilmeli: https://github.com/hedgehogqa/haskell-hedgehog/issues/473#is...
Başka bir deyişle, applicative üreteçler “üreteç sonucunu kullanarak başka bir üretece dallanmaz”; applicative’in “paralel” doğası nedeniyle küçültme optimize edilir. Buradaki paralel, yazıdaki thread anlamında değil, monadik anlamdadır. Applicative “paralel” olduğu için üreteçler bağımsız olarak küçültülebilir. Buna karşılık monad üreteçler “seri”dir; birini küçültmek, ardından gelen üretecin davranışını zorunlu olarak değiştirir
Herkese açık bir sunum varsa bağlantısını görmek isterim
Gerçekten production’a hazır olduğunu düşünmüyorum; bu da tasarım gereği böyleymiş gibi görünüyor.[0]
clojure.spec.alphayıtest.checkile birlikte ve onsuz epey kullandığım için, farklar olsa da genel fikre tamamen yabancı değildim[0] https://github.com/HypothesisWorks/hypothesis/issues/3493
Hypothesis’e benziyor ama doğrusal bir sekans yerine üreteç ağacı kullanıyor. Selective functor’a dayanıyor; bu da doğrulayıcılar gibi yerlerde de kullanışlı, iyi bir arayüz
https://hackage.haskell.org/package/falsify adresine göre bu kütüphane, iç entegre küçültmeyi destekleyen özellik tabanlı test sağlıyor. Hedgehog anlamında entegre: yani ayrı bir küçültücü ve üreteç yazmaya gerek yok. Hypothesis anlamında iç: yani monad bind genelinde de iyi çalışıyor
Özellik tabanlı testi denemeye çalıştım ama her zaman iki sandalye arasında kalmış gibi hissettim
Bir özelliği sıkı biçimde test edecek kadar iyi anlıyorsam genelde onu tip sistemine itip yapısı gereği doğru hale getirebiliyorum. Sadece basit bir smoke test istiyorsam, tek bir rastgele girdi daha kolay oluyor
Örneğin çoğu zaman yavaş ama basit, naif bir implementasyon ile optimize edilmiş bir implementasyon olur ve rastgele girdiler için ikisinin çıktısını karşılaştırabilirsiniz. Anlaması kolay, basit bir özelliktir ama genelde tip sistemine koymak zordur
Benzer şekilde girdilerin sunulma sırasının önemli olmaması gerekebilir; ya da veriyi bölebileceğiniz bir yol vardır ve
max(A의 최대값, B의 최대값) = maximum(A union B)gibi bir özellik olabilir. Böyle şeyler tip sistemine nasıl kodlanabilir?Ya da “herhangi A ve B için, A’da bulunan herhangi bir optimum çözüm, A union B’de bulunan herhangi bir optimum çözümden daha kötü değildir” gibi bir şey; veya
f(f(A)) = f(A)türünden idempotentlik de varBunların hepsi anlaşılması kolay özellikler, ama çoğu tip sisteminde ifade etmek kolay değil
Ancak ana akım tip denetleyicilerin ele alamadığı çok sayıda kısıt var. Bağımlı tipler çok yardımcı olurdu, ama hâlâ teorem kanıtlayıcılar gibi niş alanlarla sınırlı görünüyor
Listede özgün QuviQ Erlang QuickCheck eksik mi diye merak ediyorum
Tam ürün tescilli olsa da, ücretsiz sürüm QuickCheck Mini de sunuluyor: http://www.quviq.com/downloads/
Clojure’da da artık durumlu bir quickcheck kütüphanesi var: https://github.com/griffinbank/test.contract
Paralel test ilginç, ama henüz büyük bir acı kaynağı olmadı
C#/.NET testleri için CsCheck[0] kullanıyordum ve oldukça memnundum
Hedgehog veya FsCheck’e göre çok daha erişilebilirdi, hızı da epey iyiydi
[0] https://github.com/AnthonyLloyd/CsCheck
Yazıda anlatılan linearizability/paralel testleri de destekliyor
Referans:
https://github.com/AnthonyLloyd/CsCheck?tab=readme-ov-file#m...
https://github.com/AnthonyLloyd/CsCheck?tab=readme-ov-file#c...
Ayrı bir C# varyantı olması mantıklı görünüyor