1 पॉइंट द्वारा GN⁺ 2024-07-06 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • Property-based testing QuickCheck के बाद कई भाषाओं में फैल गई, लेकिन जुलाई 2024 तक कई लाइब्रेरीज़ 2009 में ही व्यवस्थित हो चुकी state-based testing और parallel testing को पर्याप्त रूप से उपलब्ध नहीं करातीं
  • मुख्य अंतर state machine मॉडल से क्रमिक state changes को verify करने और उसी मॉडल को linearisability जांच में reuse करके parallel execution में race condition खोजने की क्षमता में है
  • सर्वे में शामिल कई लाइब्रेरीज़ में state-based testing नहीं है या experimental है, और parallel testing उससे भी दुर्लभ है; FsCheck, Gopter, RapidCheck, SwiftCheck, jsverify आदि में संबंधित issues कई वर्षों से खुले पड़े हैं
  • लगभग 400 लाइनों का Haskell implementation state-based और parallel property-based testing को reproduce करता है, और पारंपरिक state machine specification के बजाय programmers के लिए परिचित fake-based reference implementation को मॉडल के रूप में इस्तेमाल करता है
  • Contract-tested fake को single component verification से आगे, real dependencies की जगह inject किए जाने वाले तेज़ और deterministic integration tests में भी reuse किया जा सकता है

QuickCheck के बाद पैदा हुआ feature gap

  • Property-based testing “test लिखो मत, generate करो” के नारे के तहत कई programming language communities में फैल गई
  • मूल Haskell लाइब्रेरी QuickCheck के Wikipedia page पर अन्य भाषाओं में इसके 57 reimplementations सूचीबद्ध हैं
  • पहला QuickCheck paper, QuickCheck: A Lightweight Tool for Random Testing of Haskell Programs, ICFP 2000 में प्रकाशित हुआ था, और पहले implementation का पूरा source paper के appendix में लगभग 300 लाइनों का code था
  • शुरुआती QuickCheck केवल pure functions को test कर सकता था, और 2002 में Testing monadic code with QuickCheck ने mutable state, file I/O, networking जैसे effects वाले code को संभालने की बुनियाद रखी

State-based और parallel testing का उदय

  • Quviq AB की स्थापना 2006 में John Hughes और Thomas Arts ने की थी, और Ericsson के Erlang projects की testing इसके शुरुआती use cases में थी
  • Erlang pure functional language नहीं है और concurrency आम है, इसलिए केवल मौजूदा monadic QuickCheck पर्याप्त रूप से सुविधाजनक नहीं था
  • Quviq के closed source Erlang QuickCheck में दो features शामिल थे जो बाद में कई open source implementations में छूट गए
    • state machine model का उपयोग करने वाली sequential state-based property-based testing
    • उसी sequential state machine model को reuse करके race condition detect करने वाली parallel testing
  • State-based testing अपने वर्तमान रूप में QuickCheck testing for fun and profit (2007) में सामने आई
  • Parallel testing को Finding Race Conditions in Erlang with QuickCheck and PULSE (ICFP 2009) में विस्तार से कवर किया गया, और Herlihy और Wing के Linearizability: a correctness condition for concurrent objects (1990) को मुख्य technique बनाया गया
  • Quviq QuickCheck का library code papers में share नहीं किया गया; public तौर पर केवल API और उस API का उपयोग करने वाले test examples उपलब्ध थे

2024 library survey के नतीजे

  • मौजूदा state-of-the-art state machine model आधारित stateful testing और उसी sequential model के साथ linearisability जोड़ने वाली parallel testing है
  • यह survey जुलाई 2024 तक documentation, issue trackers और कुछ source code पढ़कर संकलित किया गया है
  • कई libraries state-based testing उपलब्ध नहीं करातीं या सीमित रूप में उपलब्ध कराती हैं
    • QuickCheck(Haskell) में state-based testing जोड़ने का issue 2016 से खुला है
    • SwiftCheck में भी state-based testing जोड़ने का issue 2016 से खुला है
    • jsverify में state-based testing जोड़ने का issue 2015 से बाकी है
    • proptest(Rust) के लिए अलग proptest-state-machine देखना पड़ता है
  • Parallel testing support और भी दुर्लभ है
    • Gopter के README में “No parallel commands … yet?” लिखा है और 2017 का issue मौजूद है
    • FsCheck में parallel support जोड़ने का issue 2016 से खुला है
    • RapidCheck में parallel support जोड़ने का issue 2015 से खुला है
    • propcheck में 2020 से parallel testing जोड़ने का issue है
  • दोनों features support करने वाले open source examples के रूप में PropEr, Hedgehog, qcheck-stm, quickcheck-state-machine, stateful-check आदि बताए गए हैं
  • Parallel feature होने पर भी कुछ cases में limitations हैं
    • QuickTheories की parallel testing में possible end states की संख्या commands की संख्या के साथ तेजी से बढ़ती है, इसलिए source comments में लिखा है कि command list को आम तौर पर 10 या उससे कम तक सीमित रखना चाहिए
    • ScalaCheck के LevelDB और Redis examples threadCount = 1 वाले sequential examples के रूप में दिए गए हैं
    • fast-check का race condition support, Quviq QuickCheck की parallel testing के विपरीत, sequential state machine model reuse या linearisability के उपयोग जैसा नहीं दिखता
  • Parallel testing बाद में जोड़ी गई हो, ऐसा कोई स्पष्ट example नहीं दिखता; अगर इसे शुरू से API design में शामिल न किया जाए तो काफी redesign की जरूरत पड़ सकती है

Feature adoption धीमा क्यों रहा

  • John Hughes ने तीन कारण बताए
    • State-based और parallel testing pure function testing जितनी उपयोगी नहीं हैं
    • State machine model लिखने के लिए सामान्य tests से अलग सोचने का तरीका चाहिए और training की जरूरत होती है
    • केवल open source से industrial adoption अच्छी तरह नहीं हुआ; closed source product और training/consulting ने adoption में मदद की
  • Pure function के छोटे हिस्सों पर ही property-based testing करने से भी काफी लाभ मिल सकता है, लेकिन industrial systems में database, stateful protocol, concurrent data structure बहुत होते हैं, इसलिए state-based और parallel testing भी लगभग उतनी ही महत्वपूर्ण हैं
  • State-based specification हमेशा pure function specification से ज्यादा कठिन नहीं होती
    • key-value store model सिर्फ key-value pair list से भी काफी आगे तक जा सकता है
    • LevelDB case में एक simple model ने कुछ मिनटों में 17-step reduced counterexample खोज लिया, और Google के fix के बाद फिर कुछ मिनटों में 31-step counterexample खोज लिया
    • दूसरी समस्या background compaction process में bug थी; compaction read performance सुधारने और disk space reclaim करने के लिए महत्वपूर्ण है, लेकिन model में इसे explicitly शामिल नहीं किया गया था
  • भले ही closed source ने industrial adoption में मदद की हो, इसका open source adoption में योगदान नहीं रहा—ऐसा आकलन किया गया
  • Quviq QuickCheck license के बिना paper results reproduce करने के लिए काफी reverse engineering चाहिए, इसलिए इसे लगभग असंभव माना गया

प्रस्ताव: छोटा public implementation और आसान specification

  • सुधार की दिशा दो हैं
    • मूल QuickCheck के लगभग 300-line implementation की तरह, state-based और parallel property-based testing का छोटा open source implementation उपलब्ध कराना
    • State machine के बजाय programmers के लिए पहले से परिचित mock और test double concepts को reuse करके specification लिखने का बोझ घटाना
  • इस hypothesis को verify करने के लिए दो बातें दिखाई गईं
    • State-based और parallel property-based testing को लगभग 400 lines of code में implement किया गया
    • state machine के बजाय in-memory reference implementation, यानी fake, को model के रूप में इस्तेमाल किया गया

शुद्ध property-based testing का सारांश

  • शुद्ध function testing में input generate किया जाता है और यह जांचा जाता है कि function का output input के साथ कोई संबंध संतुष्ट करता है या नहीं
  • उदाहरण के लिए, reverse को किसी भी list xs के लिए reverse (reverse xs) == xs property से test किया जा सकता है
  • QuickCheck मूल रूप से 100 tests generate करता है, और fail होने पर input को shrink करके न्यूनतम counterexample पेश करता है
  • reverse xs == xs जैसी गलत property [0,1] जैसे न्यूनतम counterexample तक घटा दी जाती है
  • अक्सर दिखने वाले property patterns में inverse, idempotency, associativity, abstract data type के axiom, metamorphic property आदि शामिल हैं
    • inverse: deserialise (serialise i) == i
    • idempotency: sort (sort xs) == sort xs
    • associativity: (i + j) + k == i + (j + k)

state-based property-based testing

  • state वाले components समान input पर हमेशा समान output नहीं बनाते
    • counter के पहले incr result और दूसरे incr result पिछले state के आधार पर बदलते हैं
    • database और file system में भी पिछले inputs का इतिहास अगले output को प्रभावित करता है
  • जहां शुद्ध function testing single input से deal करती है, वहीं state-based testing input sequences generate करके जांचती है कि system समय के साथ कैसे बदलता है
  • model को m -> i -> (m, o) रूप के fake के तौर पर व्यक्त किया जाता है
    • पिछले model state m और input i से अगला model और output o calculate किया जाता है
    • हर step पर वास्तविक system के output और fake के output की तुलना की जाती है
    • mismatch होने पर input sequence को shrink करके छोटा counterexample खोजा जाता है
  • Counter उदाहरण

    • global mutable variable इस्तेमाल करने वाले Haskell counter को test target बनाया गया
    • incr counter को बढ़ाता है, और get वर्तमान value पढ़ता है
    • model के लिए एक Counter Int काफी है, और StateModel instance initial state Counter 0, Incr, Get, Incr_ (), Get_ Int, runFake, runReal, command generator define करता है
    • incr42Bug की तरह counter value 42 होने पर increment न करने वाला bug डालने पर, QuickCheck 66 tests के बाद failure ढूंढता है और 29 बार shrink करके 43 increments के बाद Get करने वाला न्यूनतम counterexample पेश करता है
    • अगर tests के बीच वास्तविक global counter को reset नहीं किया जाए, तो model हमेशा 0 से शुरू होता है लेकिन वास्तविक counter पिछले test का state बनाए रखता है, जिससे mismatch होता है
  • state-based library interface

    • StateModel interface test target system को black box मानता है और command को input, response को output रखता है
    • मुख्य components हैं Command state, Response state, initialState, runFake, runReal, generateCommand
    • optional components इस प्रकार हैं
      • Reference: file handle जैसे मामलों में, जहां पिछले response द्वारा बनाए resource को बाद के command में refer करना हो, इस्तेमाल होता है
      • PreconditionFailure: खुले हुए file handle न होने पर read न करने देने जैसी precondition failure को व्यक्त करता है
      • CommandMonad: default IO है, लेकिन कोई दूसरा monad इस्तेमाल किया जा सकता है
      • monitoring, commandName: coverage और statistics के लिए इस्तेमाल होते हैं
    • command generate करते समय वास्तविक file handle जैसी values नहीं बनाई जा सकतीं, इसलिए Var Int रूप के symbolic reference generate किए जाते हैं, और execution के दौरान वास्तविक reference से substitute किए जाते हैं
    • shrink के बाद उन commands को हटाया जाता है जो precondition तोड़ते हैं या scope के बाहर symbolic reference इस्तेमाल करते हैं
  • Circular buffer उदाहरण

    • C में लिखी circular queue को Haskell FFI से test किया गया, और model को simple list-based queue के रूप में लिखा गया
    • C implementation error checking नहीं करता, इसलिए empty queue पर get करने से uninitialized memory लौट सकती है
    • वास्तविक implementation circular index से efficient है लेकिन स्पष्ट रूप से correct नहीं है, और fake कम efficient है लेकिन testing के लिए है, इसलिए यह समस्या नहीं है
    • new queue reference लौटाता है, इसलिए model Map (Var Queue) FQueue से कई queues manage करता है
    • शुरुआत में full queue पर put करने की precondition missing थी; size 1 queue में 0, 1 डालकर get करने पर model FIFO होने के कारण 0 की उम्मीद करता है, लेकिन C code 1 लौटाता है
    • यह implementation bug नहीं बल्कि model precondition की कमी थी, इसलिए QueueIsFull precondition जोड़कर इसे fix किया गया
    • generator में Size command missing होने की बात coverage output से सामने आई, और इसे जोड़ते ही queue size calculation bug मिला
    • size 1 queue में एक item डालकर Size करने पर expected value 1 है, लेकिन actual value 0 है; new में internal buffer size को n + 1 रखने वाला fix सुझाया गया
    • इसके बाद abs(q->inp - q->outp) % q->size size 1 में pass हो जाता है, लेकिन size 2 में फिर fail होता है, और final fix (q->inp - q->outp + q->size) % q->size है
  • Die Hard 3 water jug puzzle

    • 3L और 5L के water jugs से ठीक 4L बनाने वाली puzzle को state-based testing से solve किया गया
    • वास्तविक implementation के बिना सिर्फ model और fake चलाने पर भी, किसी खास state पर पहुंचते ही test को fail कराकर shrunk action sequence प्राप्त किया जा सकता है
    • 199 tests और 11 shrinks के बाद पेश किया गया sequence यह flow है
      • 5L jug भरना
      • 5L से 3L में डालना
      • 3L खाली करना
      • 5L से 3L में फिर से डालना
      • 5L भरना
      • 5L से 3L में डालना
    • trace में intermediate states दिखते हैं, जिससे यह verify किया जा सकता है कि big jug 4L कैसे बनता है

Parallel property-based testing

  • concurrent code के bugs को reproduce करना और fix verify करना मुश्किल होता है, क्योंकि thread interleaving हर run में बदल जाता है
  • लक्ष्य यह है कि users बहुत ज़्यादा अतिरिक्त test code लिखे बिना, sequential state-based testing की तरह parallel testing कर सकें
  • counter example में अगर incr, readIORef के बाद writeIORef को non-atomically execute करता है, तो दो threads एक-दूसरे के increment को overwrite कर देते हैं और race condition पैदा होती है
  • parallel test execution के दौरान command के invocation और response के समय इकट्ठा करके concurrent history बनाता है, और जांचता है कि उस history को किसी sequential interleaving से explain किया जा सकता है या नहीं
  • अगर कोई भी एक interleaving sequential model से match करता है, तो history को linearise माना जाता है और correct समझा जाता है
  • अगर कोई भी sequential interleaving असली response को explain नहीं कर पाता, तो उसे non-linearisable result के तौर पर handle किया जाता है
  • Parallel command generation और shrink

    • parallel program को ParallelCommands और कई Fork से represent किया जाता है, और हर Fork के अंदर के commands parallel में execute होते हैं
    • example implementation single, double, और triple threaded execution को cover करता है
    • parallel execution में Fork [Write "a" "foo", Write "a" "bar"] की तरह interleaving के हिसाब से possible model state बदल सकता है
    • parallel model किसी single state के बजाय states के set के आधार पर command generation और shrink करता है
    • parallelSafe यह check करता है कि Fork के अंदर commands के सभी permutations में precondition बनी रहती है या नहीं
    • उदाहरण के लिए, अगर Write "a" और Delete "a" एक ही fork में हों, तो एक command दूसरे command की precondition को तोड़ सकता है
    • shrink process में भी केवल वही commands छोड़े जाते हैं जो precondition और symbolic reference scope को बनाए रखते हैं
  • Parallel execution और linearisability check

    • parallel execution हर command के Invoke और Ok event को history में record करता है
    • अगर response में नया reference शामिल हो, तो atomic counter से environment को expand किया जाता है ताकि threads के बीच reference number collision से बचा जा सके
    • history में सभी possible interleavings को Rose tree के रूप में enumerate किया जाता है
    • linearisable check करता है कि इस tree का कोई path sequential runFake model और response को match कराता है या नहीं
    • parallel testing आखिरकार sequential model को reuse करती है, इसलिए user sequential model लिखने के बाद थोड़े से extra code से parallel testing पा लेता है
  • Parallel counter example

    • counter की parallel testing enable करने के लिए जोड़ा गया code सिर्फ ParallelModel Counter instance और property है
    • non-atomic incrRaceCondition इस्तेमाल करने पर race condition मिलती है
    • अगर छोटे test case में भी race हो, लेकिन किसी दूसरे interleaving की वजह से failure reproduce न हो, तो QuickCheck मान सकता है कि छोटा test case pass हो गया और shrink रोक सकता है
    • सही समाधान deterministic thread scheduler है, और parallel testing paper इसी का इस्तेमाल करता है
    • example implementation एक सरल workaround के तौर पर shared memory read/write के आसपास छोटी sleep डालता है, ताकि वही interleaving होने की संभावना बढ़े
    • sleep race खोजने के लिए नहीं, बल्कि मिली हुई race के counterexample को छोटा बनाने के लिए ज़रूरी है
    • sleep जोड़ने के बाद minimal counterexample ParallelCommands [Fork [Incr,Incr],Fork [Get]] तक shrink हो जाता है
  • Process registry example

    • example के तौर पर Erlang process registry जैसी system का इस्तेमाल किया जाता है, जो thread spawn करता है और name से ThreadId को register·lookup·unregister·kill करता है
    • sequential model बनाए गए thread id, registered name-thread pair, और killed thread id को track करता है
    • Register और Unregister fail हो सकते हैं, इसलिए response में Either ErrorCall () का इस्तेमाल होता है
    • असली implementation की error location information को fake से match कराने के लिए abstractError से हटाया जाता है
    • monitoring, RegisterFailed, RegisterSucceeded, UnregisterFailed, UnregisterSucceeded coverage दिखाता है
    • जानबूझकर register में existing registry को overwrite करने वाला bug डालने पर, पहले से registered "e" को unregister न कर पाने वाला sequential counterexample आता है
    • parallel testing में लंबा counterexample आता है, और SleepyIORef इस्तेमाल करने पर यह Fork [Register "b" (Var 0), Register "c" (Var 0)] के रूप में shrink होता है
    • समस्या यह race है कि readRegistry से check करने के बाद और atomicModifyIORef call करने के बीच कोई दूसरा thread बीच में आ सकता है
    • register, unregister, kill पर global lock apply करने के बाद parallel test pass हो जाता है

Fake-आधारित मॉडल और integration test

  • पारंपरिक state machine specification with post-conditions के बजाय in-memory fake को reference implementation के रूप में इस्तेमाल किया जाता है
  • Edsko de Vries का 2019 का लेख fake को post-condition-आधारित state machine specification के ऊपर implement करने का तरीका सुझाने वाला पहला लेख बताया गया है
  • fake, mock जैसा होता है, इसलिए formal specification से परिचित न होने वाले programmers के लिए इसे आसान approach के रूप में पेश किया गया है
  • fake का एक फायदा यह भी है कि integration test में इसे dependent components की जगह इस्तेमाल किया जा सकता है
    • असली dependency को start या enable करने की ज़रूरत नहीं होती
    • ज़्यादा तेज़ और deterministic integration test बनाए जा सकते हैं
  • fake गलत हो सकता है, इस समस्या को contract test से handle किया जाता है
  • state-based और parallel property-based test fake और real implementation के मेल को verify करते हैं, इसलिए fake contract-tested dependency की भूमिका निभाता है
  • Queue fake से testing और deployment को अलग करना

    • queue interface IQueue में iNew, iPut, iGet, iSize होते हैं
    • real implementation C queue wrapper को सीधे जोड़ता है
    • fake implementation model state को IORef में store करता है और fNew, fPut, fGet, fSize के ज़रिए update करता है
    • component IQueue q interface के लिए लिखा जाता है
    • test में fake instance इस्तेमाल होता है, और deployment में real instance इस्तेमाल होता है
    • state-based property-based test से यह assumption स्थापित किया जाता है कि fake, real के प्रति faithful है
  • File system fake

    • file system interface IFileSystem h में iMkDir, iOpen, iWrite, iClose, iRead होते हैं
    • real implementation /tmp/qc-test के नीचे real file system इस्तेमाल करता है
    • fake को directory set, file content map, open handle map, और next handle वाले in-memory FakeFS के रूप में implement किया जाता है
    • fOpen, fWrite, fClose, fRead busy file, non-existent directory, closed handle जैसी precondition failure को model करते हैं
    • अगर fake file system को real file system के प्रति faithful साबित करके test किया गया हो, तो file system पर निर्भर components को fake से integration test किया जा सकता है और deployment के समय real file system से replace किया जा सकता है
    • real से replace करने पर bug आए, तो यह जांचना चाहिए कि fake और real के बीच mismatch state-based property-based test से कैसे pass हो गया
  • बड़े component system

    • A, B पर निर्भर हो और B, C पर निर्भर हो—ऐसे system को भी इसी तरीके से extend किया जाता है
    • हर component के लिए interface रखा जाता है
      • iC :: IO IC
      • iB :: IC -> IO IB
      • iA :: IB -> IO IA
    • testing strategy इस प्रकार है
      • C को state-based और parallel property-based test से verify करके contract-tested fake C प्राप्त किया जाता है
      • B के integration test में fake C इस्तेमाल होता है
      • A के test में fake B इस्तेमाल होता है, जो fake C का इस्तेमाल करता है
    • यह तरीका अधिक components या services पर भी इसी pattern में extend होता है

निष्कर्ष

  • state-based और parallel property-based testing को लगभग 400 lines के code में implement किया जा सकता है, जो shrinking न होने वाले पहले QuickCheck के लगभग 300-line implementation से तुलना करने योग्य आकार है
  • fake को model के रूप में इस्तेमाल करने से state-based और parallel testing के specifications लिखना अधिक परिचित रूप लेता है, और बड़े systems को compositional तरीके से test करने में इसे reuse किया जा सकता है
  • यदि हर language community प्रयोग जारी रखे, तो property-based testing libraries की स्थिति सुधारने की गुंजाइश है

1 टिप्पणियां

 
GN⁺ 2024-07-06
Hacker News की राय
  • coverage-based fuzzing आ चुका है और Go में इसका अच्छा support भी है, तो property-based testing library न इस्तेमाल करने पर क्या छूट जाता है, यह जानना चाहूंगा
    https://www.tedinski.com/2018/12/11/fuzzing-and-property-tes...
    नीचे दिए गए fuzz test और उससे जुड़े invariant checks को देखें, तो लगता है कि यह असल में property test जैसा ही तो है
    https://github.com/ncruces/aa/blob/505cbbf94973042cc7af4d6be...
    https://github.com/ncruces/aa/blob/505cbbf94973042cc7af4d6be...

    • property-based testing और fuzzing के बीच फर्क आम तौर पर माहौल/रुझान के आधार पर की गई मोटी grouping जैसा है
      असल में फर्क हैं, लेकिन सीमाएं काफी धुंधली हैं, और यह बिल्कुल सटीक तय करना बहुत जरूरी नहीं कि क्या fuzzing है और क्या property-based testing
      तेज चलने वाले tests और विस्तृत assertions property-based testing हैं, लंबे समय तक चलाकर सिर्फ crashes ढूंढने वाला हिस्सा fuzzing है, और बीच का क्षेत्र अस्पष्ट है
      https://hypothesis.works/articles/what-is-property-based-tes...
    • coverage-based fuzzing और property-based testing को अच्छी तरह जोड़ा जा सकता है
      Google में रहते हुए दोनों को मिलाने वाला एक internal tool वाकई बहुत अच्छा था। आप हमेशा की तरह property-based tests लिखते, और run करते समय test framework coverage पाने के लिए खास तरीके से compile करता और random inputs को adjust करके coverage बढ़ाता। बेशक यह कई machines के cluster पर पूरी तरह automatically चलता था
      पारंपरिक property-based testing आम तौर पर सिर्फ library के रूप में implement होती है, इसलिए random input generation को guide करने के लिए coverage information जरूरी नहीं कि उपलब्ध हो
    • चूंकि properties assert की जा रही हैं, इसलिए “level 1 से बड़े सभी nodes के दो children होते हैं” जैसी चीज definition के हिसाब से property-based testing में आती है
      हालांकि library के हिसाब से काफी convenience features मिल सकते हैं। ऐसी ही एक उपयोगी चीज shrinking है, और यहां के “Shrinking” section को देख सकते हैं: https://tech.fpcomplete.com/blog/quickcheck-hedgehog-validit...
      generators को compose करने वाले combinators भी शानदार होते हैं, और कुछ libraries में exception-like behavior पैदा करने वाले ज्ञात “bad” values के sets भी होते हैं
    • मुझे ठीक से नहीं पता कि Go का fuzz test linked article की बातों से कैसे अलग है, लेकिन उस article में कहा गया था कि सही fuzzers को कई दिनों या हफ्तों तक चलाना चाहिए, और property-based testing को fuzzing के मुकाबले लगभग हमेशा चुनना चाहिए
      एक कदम पीछे हटकर testing पर ज्यादा meta सवाल पूछना चाहता हूं। क्या test success का मतलब code success है और क्या इसका उल्टा भी सही है? क्या Go के contract में कहीं लिखा है कि same input को same code में डालने पर same output मिलेगा?
    • API के नजरिए से मुख्य फायदा यह है कि मनचाहे random data structures generate करने वाली combinator library मिलती है
      random objects के set को दर्शाने वाले Arbitrary type के साथ काम करने पर test inputs generate करने वाले reusable functions आसानी से लिखे जा सकते हैं। ऐसी library को Go के fuzzing framework के साथ भी काफी आसानी से इस्तेमाल किया जा सकता है
      फिर भी मुझे लगता है कि map, filter, chain, oneOf जैसे common combinators थोड़े awkward हो सकते हैं, इसलिए मैं JavaScript के लिए एक नई property testing library लिख रहा हूं। लक्ष्य इसे ज्यादा इस्तेमाल-लायक बनाना है, लेकिन यह अभी experimental है और public release से पहले की अवस्था में है
  • clojure.spec.alpha को test.check के साथ इस्तेमाल करें या नहीं, इसका अनुभव शानदार था, लेकिन Python के hypothesis को आज़माने पर यह सचमुच बहुत खराब लगा
    Hypothesis डिज़ाइन के स्तर पर सरल लेकिन “बड़े” डेटा सेट संभाल ही नहीं पाता लगता था, और यहाँ “बड़ा” भी असल में इतना बड़ा नहीं था। [0] यह इतना पीड़ादायक था कि हमने अपने काम के Python टेस्ट सूट से Hypothesis और generation-based testing को पूरी तरह हटा दिया
    [0] https://github.com/HypothesisWorks/hypothesis/issues/3493

    • इस मामले में ऐसा लगता है कि Hypothesis बड़े डेटा सेट नहीं संभाल पा रहा था, इसके बजाय वह shrink किए गए cases में से काफ़ी को reject कर रहा था
      Hypothesis ने यह देखने के लिए generated integers को 0 तक घटाने की कोशिश की कि bug 0 पर भी मौजूद है या नहीं, और test ने 0 शामिल होने की वजह से fail होने के बजाय reject कर दिया। छोटे cases में यह सिर्फ़ inefficiency थी, लेकिन बड़े cases में यह उस स्तर तक पहुँच गया जहाँ Hypothesis ने हार मान ली
      उस thread में किसी ने सुझाव दिया था कि कोई दूसरी instance-generation strategy इस्तेमाल करें जो 0 generate न कर सके। यानी Hypothesis के shrinker की पसंदीदा value generate करके उसे reject करने के बजाय, उसे शुरुआत से ही न बनाए। जिज्ञासा है कि क्या आपने वह आज़माया था
      यह भी जिज्ञासा है कि clojure.spec.alpha इसे अलग तरह से कैसे handle करता है
      https://news.ycombinator.com/item?id=40876437 पर mjaniczek की comment में इस case को Hypothesis approach की कमी बताया गया है
      भाव यह है: “generator अब byte-list parser बन जाता है जो fail हो सकता है, इसलिए थोड़ी inefficiency आती है, और user ऐसे अजीब generators बना सकता है जिन्हें internal shrinker पूरी तरह shrink नहीं कर पाता। फिर भी तीनों approaches में developer experience सबसे अच्छा है…”
      बेशक, आप शायद यह मानने से सहमत नहीं होंगे कि आपका test “अजीब” तरीके से लिखा गया था
    • Clojure का spec अच्छा था क्योंकि उसके आसपास चीज़ें compose करना सचमुच आसान था, लेकिन Elixir पर जाने के बाद ऐसे tests लिखने के लिए मुझे पुराने Erlang library propEr तक उतरना पड़ा। काफ़ी निराशाजनक
    • GitHub issue में दिया गया example filter को ऐसे तरीके से इस्तेमाल कर रहा है जिससे वही समस्या पैदा हो रही है
      अगर आप randomly generate करके फिर किसी property से match करने वाली चीज़ों को छाँटते हैं, तो generation process में आप असल में लॉटरी scratch कर रहे होते हैं
  • लेख में पूछे गए सवाल “published research को open source tools, या कम से कम public और दूसरे researchers के लिए मुफ्त उपलब्ध tools से reproducible बनाने की requirement क्यों नहीं है?” का सरल जवाब यह है कि ऐसी requirement का तत्काल परिणाम यह होगा कि जो papers इस शर्त को पूरा नहीं करते, वे publish नहीं होंगे
    उदाहरण के लिए, Quviq QuickCheck paper जैसा paper भी publish नहीं हुआ होता, जो लगता है कि authors और दूसरों के लिए उपयोगी था, और community उस जानकारी के उपहार से वंचित रह जाती

    • अच्छा होगा अगर कुछ publishers reproducibility मांगें और कुछ न मांगें
      हर requirement का exclusion effect होता है, और ऐसे borderline cases हमेशा होते हैं जहाँ paper useful हो सकता है भले ही वह requirements पूरी न करे
    • यह कोई साफ़-सुथरा, दो-टूक सवाल नहीं है, और इसे राजनीतिक सवाल तक कहा जा सकता है, लेकिन इसलिए यह defence logic बहुत valid नहीं हो जाता
      अगर इस logic को valid मानें, तो इसे ढाल बनाकर कहीं तक भी जाया जा सकता है। Reproducibility को requirement से हटा दें तो फिर जिन चीज़ों को explain नहीं करना चाहते, उनमें से कुछ भी explain करने की ज़रूरत नहीं। Samples का data भी नहीं, statistical significance test भी नहीं देना पड़ेगा। किसी result को हासिल करने का दावा करने वाला अस्पष्ट abstract ही काफ़ी हो जाएगा
      यहाँ तक कि Fermat ने अपनी निजी copy of Arithmetica के margin में जो मशहूर note छोड़ा था, वह भी पूरी तरह valid research paper बन जाएगा। आखिर हम यह मूल्यवान जानकारी खोना नहीं चाहेंगे कि एक प्रसिद्ध mathematician को लगा था कि उसके पास किसी theorem का concise और elegant proof है। बेशक, असल में उसके पास होने की संभावना कम ही थी
      इस राजनीतिक सवाल पर मेरी राय यह है कि मौजूदा standards बहुत ढीले हैं। किसी को भी कुछ publish करने के लिए मजबूर नहीं किया जाता। दुनिया में proprietary value जैसी वजहों से कहीं भी publish न होने वाली research बहुत है, और ऐसी research गायब नहीं होने वाली
      लेकिन अगर आप academia में काम करते हैं, और ऊपर से research funding भी लेते हैं, और कहते हैं कि आपका लक्ष्य दुनिया के scientific knowledge को आगे बढ़ाना है, तो आपसे वास्तव में उसी लक्ष्य का पालन करने की मांग करना fair है। सिर्फ़ academic career ladder चढ़ने के लिए उस लक्ष्य का पालन करने का दिखावा नहीं करना चाहिए
    • मुझे लगता है कि source code को सिर्फ़ reviewers को disclose करने का तरीका भी संभव हो सकता है
      code चलाने के लिए जिन चीज़ों की ज़रूरत है, वे भी साथ में देनी होंगी। शायद वे पहले से ऐसा कर भी रहे हों
    • क्योंकि reproducibility scientific method की cornerstone है
    • Papers इसलिए publish होते हैं क्योंकि authors अपना “importance index” बढ़ाना चाहते हैं, और यह compensation और academic career prospects से बहुत सीधे तौर पर जुड़ा है
      उस उद्देश्य के लिए requirements और बढ़ जाएँ तो भी published papers की संख्या घटने की संभावना कम है
      Published papers की ज्यादा गंभीर समस्या यह है कि जितना हो सके उतना ज्यादा और जल्दी publish करने के लिए गलतियों को जानबूझकर नज़रअंदाज़ करना अक्सर होता है। अगर papers को verify करना आसान हो जाए तो इस स्थिति में सुधार की संभावना है, लेकिन मैं बहुत उम्मीद नहीं करूँगा। लोग shortcuts खोजने में बेहद माहिर होते हैं
  • Rust के proptest से मैं stateful property tests काफ़ी अक्सर लिखता हूँ, और आम तौर पर उन्हें सीधे code करना काफ़ी आसान होता है
    6 bugs खोजने वाला एक non-trivial उदाहरण https://github.com/sunshowers-code/buf-list/blob/main/src/cu... पर है
    Parallel tests कभी-कभी उपयोगी हो सकते हैं, लेकिन कई बार बहुत सारे tests को parallel में चलाना ज़्यादा आसान होता है

    • Rust में मैं बहुत सारे manual property tests लिखता हूँ, और वे ज़्यादातर इसी तरह के होते हैं
      Top level पर असली randomness इस्तेमाल करता हूँ, और उसके नीचे कई nested loops रखता हूँ ताकि कम complexity वाले cases से ज़्यादा complexity वाले cases तक जाया जा सके। फिर एक seed बनाकर print कर देता हूँ, जिसे deterministic pseudo-random generator में डाला जाएगा। अगर test fail हो जाए, तो error seed को copy-paste करके failed case reproduce किया जा सकता है
      मुझे लगा कि ऐसे manual property tests किसी भी framework या library से तेज़, ज़्यादा flexible और कुल मिलाकर कम झंझट वाले हैं
      हालांकि सच में robust concurrency testing के लिए मैं AWS Shuttle library (https://github.com/awslabs/shuttle) की ज़ोरदार सिफारिश करता हूँ। यह यकीन न होने लायक जटिल race conditions पकड़ सकती है। मैंने एक छोटा tutorial भी लिखा है: https://grantslatton.com/shuttle
      AWS में, AWS S3 को चलाने के लिए लिखे गए custom file system को verify करने में इस library का इस्तेमाल किया गया था
  • Link किए गए “Testing Telecoms Software with Quviq QuickCheck” paper को मैंने जल्दी से देखा, लेकिन “इन stateful operations को सीधे खुद बनाना बेहतर क्यों नहीं है?” इस सवाल का जवाब तुरंत नहीं दिखा
    मूल लेख key-value store के key-value pair model के ज़रिए इस हिस्से की ओर इशारा करता है, लेकिन समझ नहीं आता कि बस state machine क्यों न लिखी जाए, framework की ज़रूरत क्यों है। पिछले हफ़्ते काम पर file system interactions test करने के लिए मैंने सचमुच यही किया, और अंत में यह लगभग type Instruction = | Read of stuff | Write of stuff | Seek of stuff | … जैसा निकला
    तब property होती है: “commands की यह list दिए जाने पर, …”। StateModel type भी मूल रूप से वही काम मांगता है। यह कहना मुश्किल है कि StateModel अपना काम कर रहा है; इसके बदले समझने के लिए बहुत सारा framework code और जुड़ जाता है, और वास्तविक अनुभव में बस बहुत कम test code हटाने का फायदा मिलता है

    • कुछ tests के लिए यह आकलन सही है, लेकिन failed cases को shrink करना अक्सर मुश्किल हिस्सा होता है
      अगर आप केवल “valid” state transition sequences generate करना चाहते हैं, तो आम तौर पर model state चाहिए होती है, जो तय करे कि किसी खास state में कौन सा test step valid है। साथ ही shrinking के दौरान test steps हटाते समय यह भी सुनिश्चित करना पड़ता है कि हर step generate करते वक्त रखी गई preconditions टूटकर false failure पैदा न करें
      अगर किसी भी state में कोई भी operation valid है और आप पूरी तरह random arbitrary operation sequences ही चाहते हैं, तो stateful proptest framework overkill हो सकता है। लेकिन अगर model state maintain करनी हो और कई operations की preconditions specify करनी हों, तो dedicated framework बहुत काम कम कर देता है
      पिछले साल मैंने इसी विषय पर blog post लिखा था; अगर आपको ज़्यादा गहराई वाला example चाहिए तो देख सकते हैं: https://readyset.io/blog/stateful-property-testing-in-rust
      जैसा कि दूसरों ने कहा, parallel state machine testing भी dedicated framework से मिलने वाला एक अच्छा फायदा है, लेकिन यह इकलौता फायदा नहीं है
    • Stateful हिस्से को model-based testing बेहतर संभालती है, ऐसा मेरा मानना है
      Test styles को मिलाकर भी इस्तेमाल किया जा सकता है। आखिर code आपका है
    • Parallel QuickCheck को मैं इस तरह समझता हूँ कि वह check करता है कि multi-threaded program में possible सभी interleavings अंततः ऐसी state बनाते हैं या नहीं, जो commands को sequentially call करने पर भी reachable हो
      यही इसका फायदा है
  • लेखक property-based testing के state machine और parallel पहलुओं पर ध्यान देते हैं, लेकिन इसके दूसरे पहलू भी हैं जिनका असर बड़ा हो सकता है
    एक है coverage-guided property-based testing, और इसके लिए Dan Luu का लेख देखें: https://danluu.com/testing/
    दूसरा वह है जिसकी तरफ मेरा झुकाव है: values generate करते समय बनाए गए सभी invariants को बनाए रखते हुए shrinking को automate करना
    संक्षेप में, values पर काम करने वाले QuickCheck-शैली के derived shrink functions (shrink : a -> [a]) में constraints और समस्याएँ होती हैं, जिससे लोग समस्या से निपटने के बजाय shrinking बंद कर देते हैं
    Rose tree “integrated shrinking” (जैसे Hedgehog) generator की constraints का पालन करता है, लेकिन monad bind, यानी generator के result का उपयोग करके किसी दूसरे generator में branch करने पर समस्या आती है
    जादू की तरह “बस काम कर जाने वाला” दिखने वाला इकलौता तरीका Hypothesis की internal shrinking है। यह value को सीधे घटाने के बजाय random choices की list को घटाने वाली एक indirect layer इस्तेमाल करता है। downside यह है कि generator अब एक byte-list parser बन जाता है जो fail हो सकता है, जिससे थोड़ी inefficiency आती है, और user ऐसे अजीब generators बना सकते हैं जिन्हें internal shrinker पूरी तरह reduce नहीं कर पाता। फिर भी इन तीनों तरीकों में developer experience सबसे अच्छा है, और यह सोचते हुए कि लोगों का tests लिखना ही अपने-आप में एक छोटा चमत्कार है, test library author के रूप में यही approach build करने लायक सबसे ज्यादा लगती है

    • Rose tree “integrated shrinking” (जैसे Hedgehog) generator की constraints का पालन करता है लेकिन monad bind में समस्या होती है—यह हिस्सा, मेरी amateur जानकारी की सीमा में, monad bind/generator की बुनियादी सीमा लगता है
      इसके बजाय optimal shrinking के लिए applicative generators को prefer करना चाहिए: https://github.com/hedgehogqa/haskell-hedgehog/issues/473#is...
      दूसरे शब्दों में, applicative generator “generator के result का उपयोग करके दूसरे generator में branch” नहीं करता, और applicative के “parallel” स्वभाव के कारण shrinking optimize होती है। यहाँ parallel का मतलब लेख वाले threading अर्थ में नहीं, बल्कि monadic अर्थ में है। applicative “parallel” होता है, इसलिए generators को independently shrink किया जा सकता है। इसके उलट monadic generator “serial” होता है, इसलिए किसी एक को shrink करने पर उसके बाद आने वाले generator का व्यवहार अनिवार्य रूप से बदल जाता है
      अगर कोई public talk है तो उसका link देखना चाहूँगा
    • व्यक्तिगत रूप से Hypothesis मेरे लिए “बस काम कर जाने” से काफी दूर था
      मुझे नहीं लगता कि यह सचमुच production-ready है, और वह भी design से ऐसा लगता है।[0]
      मैंने clojure.spec.alpha को test.check के साथ और उसके बिना काफी इस्तेमाल किया है, इसलिए अगर फर्क हों भी तो मैं सामान्य idea से पूरी तरह अनजान नहीं था
      [0] https://github.com/HypothesisWorks/hypothesis/issues/3493
    • मेरा पसंदीदा framework falsify है, और यह “internal integrated shrinking” देता है
      यह Hypothesis जैसा है, लेकिन linear sequence के बजाय generator tree इस्तेमाल करता है। यह selective functor पर आधारित है, जो validators जैसी चीज़ों के लिए भी उपयोगी एक अच्छा interface है
      https://hackage.haskell.org/package/falsify के अनुसार, यह library internal integrated shrinking को support करने वाली property-based testing देती है। Hedgehog के अर्थ में integrated, यानी अलग shrinker और generator लिखने की जरूरत नहीं; और Hypothesis के अर्थ में internal, यानी यह monad bind के across भी अच्छे से काम करता है
  • मैंने property-based testing आज़माने की कोशिश की, लेकिन हमेशा ऐसा लगा जैसे मैं दो कुर्सियों के बीच फँसा हूँ
    अगर मैं किसी property को strict तरीके से test करने लायक अच्छी तरह समझता हूँ, तो आम तौर पर उसे type system में डालकर construction by design true बना सकता हूँ। अगर मुझे बस एक simple smoke test चाहिए, तो कोई arbitrary input ज्यादा आसान है

    • जानना चाहूँगा कि आपके मन में किस तरह की properties हैं
      उदाहरण के लिए, अक्सर एक धीमी लेकिन simple naive implementation और एक optimized implementation होती है, और arbitrary input पर दोनों के outputs compare किए जा सकते हैं। यह समझने में आसान simple property है, लेकिन आम तौर पर इसे type system में डालना मुश्किल है
      इसी तरह input किस order में दिया गया है यह मायने नहीं रखना चाहिए, या data को बाँटने का कोई तरीका हो सकता है जिससे max(A का maximum, B का maximum) = maximum(A union B) जैसी property हो। ऐसी चीज़ों को type system में कैसे encode किया जा सकता है?
      या “किसी भी A और B के लिए, A में मिली कोई optimal solution, A union B में मिली किसी optimal solution से बदतर है” जैसा कुछ, या f(f(A)) = f(A) जैसी idempotence भी होती है
      ये सब समझने में आसान properties हैं, लेकिन अधिकतर type systems में इन्हें व्यक्त करना आसान नहीं है
    • अगर संभव हो तो compile time पर constraints enforce करना निश्चित रूप से बेहतर है
      लेकिन बहुत-सी constraints ऐसी हैं जिन्हें mainstream type checkers handle नहीं कर पाते। dependent types काफी मदद करेंगे, लेकिन अभी भी वे theorem provers जैसी niche चीज़ों तक सीमित लगते हैं
  • सोच रहा हूँ कि list में original QuviQ Erlang QuickCheck छूट तो नहीं गया
    पूरा product proprietary है, लेकिन free version QuickCheck Mini भी उपलब्ध है: http://www.quviq.com/downloads/

  • Clojure में भी अब stateful quickcheck library है: https://github.com/griffinbank/test.contract
    parallel testing दिलचस्प है, लेकिन अभी तक यह बड़ी pain point नहीं रही है

  • C#/.NET टेस्टिंग के लिए मैंने CsCheck[0] इस्तेमाल किया है और उससे काफी संतुष्ट रहा हूँ
    यह Hedgehog या FsCheck की तुलना में कहीं ज़्यादा approachable था और speed भी काफी अच्छी है
    [0] https://github.com/AnthonyLloyd/CsCheck

    • लेख को और पढ़ने पर पता चला कि CsCheck property-based testing के साथ-साथ “stateful”/“model-based” testing को भी support करता है
      लेख में बताई गई linearizability/parallel testing को भी support करता है
      संदर्भ:
      https://github.com/AnthonyLloyd/CsCheck?tab=readme-ov-file#m...
      https://github.com/AnthonyLloyd/CsCheck?tab=readme-ov-file#c...
    • FsCheck बेहतरीन है, लेकिन यह F# के लिए बहुत ज़्यादा specialized है। इसी वजह से इसका नाम भी FsCheck है
      इसका एक अलग C# variant होना वाजिब लगता है