1 puan yazan GN⁺ 2024-07-06 | 1 yorum | WhatsApp'ta paylaş
  • 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 = 1 olan 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 bir xs listesi için reverse (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 == xs gibi 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)

Durum tabanlı property-based testing

  • Durum içeren bileşenlerde aynı girdi her zaman aynı çıktıyı üretmez
    • Bir sayacın ilk incr sonucuyla ikinci incr sonucu önceki duruma bağlı olarak değişir
    • Database ve file system de önceki girdi geçmişinin sonraki çıktıyı etkilemesine neden olur
  • 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 m ve girdi i üzerinden sonraki model ve çıktı o hesaplanı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
  • Counter örneği

    • Global mutable variable kullanan bir Haskell counter test hedefi olarak alınır
    • incr sayacı artırır, get ise mevcut değeri okur
    • Model için tek bir Counter Int yeterlidir ve StateModel instance'ı Counter 0, Incr, Get, Incr_ (), Get_ Int, runFake, runReal, command generator tanımlar
    • incr42Bug gibi, 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 reset edilmezse 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ü

    • StateModel arayü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ır
      • PreconditionFailure: örneğin açık olmayan bir file handle üzerinden read yapılamamasını ifade eden precondition hatasını temsil eder
      • CommandMonad: varsayılan IO'dur ama başka monad'lar da kullanılabilir
      • monitoring, commandName: coverage ve istatistikler için kullanılır
    • Command üretilirken gerçek file handle gibi değerler oluşturulamayacağı için Var Int biç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
    • new bir queue reference döndürdüğü için model, birden çok queue'yu yönetmek üzere Map (Var Queue) FQueue kullanır
    • Başta dolu queue'ya put yapmayı engelleyen precondition eksiktir; bu yüzden boyutu 1 olan bir queue'ya 0, 1 konup ardından get çağrıldığında model FIFO olduğu için 0 beklerken C kodu 1 döndürür
    • Bunun nedeni bir implementasyon bug'ı değil, modeldeki precondition eksikliğidir; QueueIsFull precondition'ı eklenerek düzeltilir
    • Size command'ı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 olarak new içinde dahili buffer boyutunun n + 1 yapılması önerilir
    • Sonrasında abs(q->inp - q->outp) % q->size ifadesi 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->size olur
  • 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, readIORef sonrası writeIORef iş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, ParallelCommands ve birden çok Fork ile ifade edilir; her Fork iç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, Fork içindeki command'lerin tüm permutation'larında precondition'ın korunup korunmadığını kontrol eder
    • örneğin Write "a" ile Delete "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 çalıştırma ve linearisability denetimi

    • paralel çalıştırma, her command'in Invoke ve Ok event'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 Rose tree olarak listelenir
    • linearisable, bu tree'deki herhangi bir path'in sıralı runFake modeli 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
  • Parallel counter örneği

    • counter için paralel testi etkinleştirmek amacıyla eklenen kod yalnızca ParallelModel Counter instance'ı ve property'dir
    • atomik olmayan incrRaceCondition kullanı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
  • Process registry örneği

    • Erlang process registry gibi, thread spawn eden ve ThreadId değerlerini isimle register·lookup·unregister·kill eden 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
    • Register ve Unregister başarısız olabileceği için response'ta Either ErrorCall () kullanılır
    • gerçek uygulamadaki error location bilgisi, fake ile eşleştirmek için abstractError ile kaldırılır
    • monitoring, RegisterFailed, RegisterSucceeded, UnregisterFailed, UnregisterSucceeded coverage'ı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 SleepyIORef kullanıldığında Fork [Register "b" (Var 0), Register "c" (Var 0)] biçimine shrink edilir
    • sorun, readRegistry ile kontrol yapıldıktan sonra atomicModifyIORef çağrılana kadar başka bir thread'in araya girebilmesinden kaynaklanan bir race'tir
    • register, unregister ve kill için global lock uygulandıktan sonra paralel test geçer

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, iSize içerir
    • Gerçek implementasyon, C queue wrapper’ını doğrudan bağlar
    • fake implementasyon, model durumunu IORef içinde saklar ve fNew, fPut, fGet, fSize üzerinden günceller
    • Bileşen, IQueue q arayüzüne karşı yazılır
    • Testlerde fake instance’ı, dağıtımda ise real instance’ı kullanılır
    • Durum tabanlı property-based test ile fake’in real’e faithful olduğu varsayılır
  • File system fake

    • file system arayüzü IFileSystem h, iMkDir, iOpen, iWrite, iClose, iRead içerir
    • Gerçek implementasyon, /tmp/qc-test altı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 FakeFS olarak 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
  • 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 IC
      • iB :: IC -> IO IB
      • iA :: 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

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

    • Property-based test ile fuzzing arasındaki ayrım çoğunlukla havaya göre yapılan kaba bir gruplamaya yakın
      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...
    • Kapsama dayalı fuzzing ile property-based test gayet birleştirilebilir
      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
    • Özellikleri assert ettiğine göre, tanım gereği “seviyesi 1’den büyük olan tüm düğümlerin iki çocuğu vardır” gibi bir property-based test sayarım
      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
    • Go’nun fuzz testinin bağlantı verilen yazıdaki içerikten nasıl farklı olduğunu pek bilmiyorum; ama o yazıda düzgün bir fuzzer’ın günlerce ya da haftalarca çalıştırılması gerektiği ve property-based testin neredeyse her zaman fuzzing’e tercih edilmesi gerektiği söyleniyordu
      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ı?
    • API açısından esas elde ettiğiniz şey, istediğiniz rastgele veri yapısını üreten bir combinator kütüphanesidir
      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.check ile birlikte kullanılsa da kullanılmasa da harika bir deneyimdi; Python’daki hypothesisi deneyince gerçekten berbat buldum
    Hypothesis, 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

    • Bu durumda Hypothesis’in büyük veri kümelerini ele alamamasından ziyade, küçültülmüş örneklerin önemli bir kısmını reddediyor olması gibi geliyor
      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 ediyorum
      https://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
    • Clojure’daki spec’in etrafında yapı kurmak gerçekten kolay olduğu için hoşuma gidiyordu; Elixir’e geçince böyle testler yazmak için eski bir Erlang kütüphanesi olan propEr’a kadar inmek zorunda kaldım. Epey hayal kırıklığı
    • GitHub issue’sundaki örnek, filterı kendi sorununu yaratacak şekilde kullanıyor
      Rastgele ü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

    • Bazı yayınevleri yeniden üretilebilirlik şartı koşsa, bazıları koşmasa iyi olurdu
      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 net biçimde kesilip atılabilecek bir soru değil; hatta siyasi bir soru bile denebilir, ama bu savunma mantığı yine de pek geçerli değil
      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
    • Kaynak kodunu yalnızca hakemlere açmak da mümkün olabilir diye düşünüyorum
      Kodu çalıştırmak için gerekenleri de birlikte sağlamak yeterli. Belki zaten böyle yapıyorlardır
    • Çünkü yeniden üretilebilirlik, bilimsel yöntemin temel taşlarından biridir
    • Makaleler, yazarlar “önem endekslerini” yükseltmek istediği için yayımlanır; bu da ücret ve akademik kariyer olanaklarıyla çok doğrudan bağlantılıdı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 proptest ile durum içeren property testleri oldukça sık yazıyorum; genelde doğrudan kodlamak epey basit oluyor
    6 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

    • Rust'ta çok sayıda manuel property testi yazıyorum ve genelde şu biçimde 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. StateModel tü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üyor

    • Bunun doğru olduğu testler de var, ama başarısız vakaları küçültme kısmı çoğu zaman zor oluyor
      Yalnı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 proptest framework'ü 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ır
      Geç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
    • Durum içeren kısmı model tabanlı testin daha iyi ele aldığını düşünüyorum
      Test stillerini karıştırıp kullanabilirsiniz. Kod sizin kodunuz
    • Paralel QuickCheck'i, çok iş parçacıklı programlarda mümkün olan tüm interleaving'lerin, nihayetinde komutlar sırayla çağrıldığında da ulaşılabilir bir durum oluşturup oluşturmadığını denetlemek olarak anlıyorum
      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ıyor
    Rose 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

    • Rose tree “entegre küçültme”nin (örn. Hedgehog) üretecin kısıtlarını izlediği ama monad bind ile sorun yaşadığı kısmı, amatör bilgimin sınırları içinde monad bind/üreteç için temel bir sınırlama olarak görüyorum
      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
    • Kişisel olarak Hypothesis benim için “kendiliğinden çalışıyor” olmaktan çok uzaktı
      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.alphatest.check ile 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
    • Benim tercih ettiğim framework falsify; “iç entegre küçültme” sunuyor
      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

    • Ne tür özellikleri kastettiğini merak ediyorum
      Ö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 var
      Bunların hepsi anlaşılması kolay özellikler, ama çoğu tip sisteminde ifade etmek kolay değil
    • Mümkünse kısıtları derleme zamanında zorlamak kesinlikle daha iyi
      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