म्यूटेशन टेस्टिंग (Mutation testing) टेस्ट सूट (test suite) की गुणवत्ता की जांच करने की एक विधि है, जिसमें जानबूझकर कोड में बग (bugs) डाले जाते हैं और यह सुनिश्चित किया जाता है कि टेस्ट उन बग्स को पकड़ लें।
जो बग डाले जाते हैं वे बहुत सीधे (straightforward) होते हैं। निम्नलिखित उदाहरणों पर विचार करें:
// original function
function mint() external payable {
require(msg.value >= PRICE, "insufficient msg value");
}
// mutated function
function mint() external public {
require(msg.value < PRICE, "insufficient msg value");
}
ऊपर दिए गए उदाहरण में, inequality ऑपरेटर को पलट दिया गया है। यदि यूनिट टेस्ट (unit tests) अभी भी पास हो जाते हैं, तो इसका मतलब है कि यूनिट टेस्ट केवल झूठा आश्वासन (false assurance) दे रहे हैं।
यह महत्वपूर्ण है कि बग्स सिंटैक्टिकली (syntactically) मान्य हों, यानी उनसे अभी भी संकलित (compile) होने योग्य Solidity कोड प्राप्त हो। यदि कोड कंपाइल नहीं होता है, तो यूनिट टेस्ट चलाना संभव नहीं होगा।
बिना टेस्टिंग के Line Coverage
आइए उस डिफ़ॉल्ट उदाहरण का उपयोग करें जो forge init चलाने के बाद Foundry प्रदान करता है, और assert स्टेटमेंट्स को कमेंट आउट कर दें।
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/Counter.sol";
contract CounterTest is Test {
Counter public counter;
function setUp() public {
counter = new Counter();
counter.setNumber(0);
}
function testIncrement() public {
counter.increment();
//assertEq(counter.number(), 1);
}
function testSetNumber(uint256 x) public {
counter.setNumber(x);
//assertEq(counter.number(), x);
}
}
यदि हम forge coverage चलाते हैं, तो हमें निम्नलिखित तालिका मिलती है:

कथित तौर पर, कोई assert स्टेटमेंट न होने के बावजूद Counter.sol पर हमारे पास 100% line और branch coverage है! इसका मतलब है कि हम अपनी मर्जी से बग डाल सकते हैं और टेस्ट फिर भी पास हो जाएंगे।
अब बेशक, यह इसका एक ज्वलंत उदाहरण है कि क्या नहीं करना चाहिए। लेकिन कवरेज को ऑप्टिमाइज़ करते समय गलती से यह चूक होना आसान है। कवरेज आपको केवल यह बताता है कि आपने कोड चलाया और यह रिवर्ट नहीं हुआ। आपको यह सुनिश्चित करना चाहिए कि सभी अपेक्षित स्टेट परिवर्तन (state changes) वास्तव में हो रहे हैं (अधिक जानकारी के लिए Solidity unit testing best practices पर हमारी अन्य पोस्ट देखें)।
म्यूटेंट्स के प्रकार
यहाँ कुछ प्रकार के म्यूटेशन्स (mutations) दिए गए हैं जो उपयोगी हो सकते हैं:
- फंक्शन मॉडिफायर्स (function modifiers) को डिलीट करना
- inequality तुलनाओं को पलटना
- कॉन्स्टेंट (constant) वैल्यू बदलना या स्ट्रिंग कॉन्स्टेंट्स को खाली स्ट्रिंग्स (empty strings) से स्वैप करना
- true को false से बदलना
&&को||से और बिटवाइज़&को बिटवाइज़|से बदलना- अर्थमैटिक (arithmetic) ऑपरेटरों को स्वैप करना (उदा.
+का-बन जाना) - लाइन्स को डिलीट करना
- लाइन्स को स्वैप करना
ऑटोमैटिक म्यूटेशन टेस्टिंग
ऊपर दिए गए नियमों के अनुसार मैन्युअल रूप से कोड को म्यूटेट करना और फिर टेस्ट सूट चलाना काफी थकाऊ काम होगा। इसलिए, ऐसे टूल्स मौजूद हैं जो यह काम स्वचालित रूप से (automatically) करते हैं। वे दर्जनों संभावित म्यूटेशन्स उत्पन्न करते हैं, कोड को म्यूटेट करते हैं, टेस्ट सूट चलाते हैं, परिणामों को स्टोर करते हैं, और बाद में एक रिपोर्ट जनरेट करते हैं। इसके तीन परिणाम हो सकते हैं:
- म्यूटेंट बच गया (mutant survived)
- समतुल्य म्यूटेंट (equivalent mutant)
- म्यूटेंट मारा गया (mutant killed)
म्यूटेंट बच गया का मतलब है कि कोड बदला गया था और टेस्ट अभी भी पास हो गया। एक समतुल्य म्यूटेंट तब होता है जब म्यूटेशन चलाने के बाद बाइटकोड (bytecode) नहीं बदलता है। ऐसा तब हो सकता है जब किसी सिंबल को रैंडमली उसी सिंबल से बदल दिया जाए, या म्यूटेशन बिज़नेस लॉजिक को नहीं बदलता है और कंपाइलर ऑप्टिमाइज़ेशन इस बदलाव को नज़रअंदाज़ कर देता है।
यहाँ एक उदाहरण दिया गया है जहाँ एक equivalent mutant हो सकता है:
// before
x = x + 1;
y = y + 1;
// after
y = y + 1;
x = x + 1;
कुछ परिस्थितियों में, इस तरह के म्यूटेशन के बाद कंपाइलर समान बाइटकोड उत्पन्न कर सकता है। यह एक equivalent mutation है। Equivalent mutants अनावश्यक या डेड कोड (dead code) का संकेत दे सकते हैं जैसे कि निम्नलिखित उदाहरण में:
require(false);
// anything that happens here doesn't matter
अंततः, mutant killed परिदृश्य ही वांछनीय है। इसका मतलब है कि कोड को म्यूटेट किया गया था और टेस्ट फेल हो गए। इसलिए, टेस्ट वास्तव में यह पता लगा सकते हैं कि कब कुछ गलत होता है। यदि म्यूटेशन के परिणामस्वरूप नॉन-कंपाइलिंग कोड प्राप्त होता है, उदा. किसी ऐसे वेरिएबल डिक्लेरेशन को डिलीट करना जिसका उपयोग बाद में किया गया हो, तो म्यूटेंट को मारा गया माना जाता है।
म्यूटेशन टेस्टिंग के लिए 100% Line और Branch Coverage महत्वपूर्ण है
यदि कोई लाइन या ब्रांच कवर नहीं की गई है, तो स्वाभाविक रूप से इस लाइन को म्यूटेट करने से टेस्ट फेल नहीं होगा।
निम्नलिखित उदाहरण पर विचार करें:
function mint(address to_, string memory questId_) public onlyMinter {
// business logic
}
यहाँ onlyMinter मॉडिफायर के साथ एक अंतर्निहित (implied) ब्रांच है। यदि इसका परीक्षण केवल ऐसी स्थिति में किया जाता है जहाँ फ़ंक्शन को कॉल करने वाला minter ही था, तो onlyMinter को डिलीट करने से टेस्ट फेल नहीं होगा। यदि onlyMinter मॉडिफायर गैर-मिनटर्स (non-minters) को ब्लॉक नहीं करता है, तो यूनिट टेस्ट इसे पकड़ नहीं पाएंगे।
वैसे, यह उदाहरण भले ही कृत्रिम लगे, लेकिन यह एक वास्तविक codearena report से लिया गया है।
Off by One Errors और Boundry Conditions
म्यूटेशन टेस्ट्स ऑफ-बाय-वन एरर्स (off-by-one errors) को पकड़ने के लिए उपयोगी हो सकते हैं। निम्नलिखित म्यूटेशन पर विचार करें:
uint256 public LIMIT = 5;
// original
function mint(uint256 amount) external {
require(amount < LIMIT, "exceeds limit");
}
// mutation
function mint(uint256 amount) external {
require(amount <= LIMIT, "exceeds limit");
}
यदि हमारे यूनिट टेस्ट्स amount को 3 और 8 सेट करते हैं, तो इस टेस्ट के संबंध में कोड में 100% branch coverage होगा। हालाँकि, म्यूटेशन टेस्ट फेल हो जाएंगे क्योंकि strict inequality को inequality से बदल दिया गया था और टेस्ट फिर भी पास हो गया। ऐसा इसलिए है क्योंकि टेस्ट्स इच्छित कार्यक्षमता (intended functionality) को सटीक रूप से व्यक्त नहीं करते हैं। विशेष रूप से, टेस्ट्स को यह लागू करना चाहिए कि ऊपरी सीमा 4 है या 5। amount के लिए 3 या 8 जैसी वैल्यूज का परीक्षण करना इस फ़ंक्शन के लिए स्मार्ट कॉन्ट्रैक्ट स्पेसिफिकेशन को पूरी तरह से परिभाषित नहीं करता है।
Vertigo-rs
RareSkills सक्रिय रूप से Solidity के लिए एक म्यूटेशन टेस्टिंग टूल, vertigo-rs को मेंटेन करता है। इसे vertigo रेपो से फोर्क (fork) किया गया था जिसे अब मेंटेन नहीं किया जाता है। इसमें Foundry फ्रेमवर्क के लिए सपोर्ट जोड़ा गया है। यह टूल Foundry, Hardhat और Truffle के साथ काम करता है। टूल को चलाने के निर्देश Readme में हैं। Solidity कोडबेस या टेस्ट्स में किसी भी संशोधन की आवश्यकता नहीं है। बस रिपॉजिटरी को क्लोन करें, डिपेंडेंसीज इंस्टॉल करें, और फिर इसे उस Solidity प्रोजेक्ट में चलाएं जिसका आप परीक्षण कर रहे हैं।
अन्य म्यूटेशन टेस्टिंग टूल्स
हालाँकि vertigo-rs एकमात्र टूल है जो स्वचालित रूप से टेस्ट सूट चलाता है, म्यूटेशन उत्पन्न करने के लिए अन्य उल्लेखनीय टूल्स भी हैं (लेकिन वे स्वचालित रूप से टेस्ट सूट को फिर से चलाने और परिणामों को संक्षेप में प्रस्तुत करने का समर्थन नहीं करते हैं)।
- Certora द्वारा Gambit
- sambucha द्वारा Universal Mutator
अन्य टूल्स भी हैं, लेकिन स्पष्ट रूप से अब उन्हें मेंटेन नहीं किया जाता है।
म्यूटेशन स्कोर
Solidity के अलावा अन्य भाषाओं के टूल कभी-कभी mutation score प्रदान करते हैं। यह मारे गए म्यूटेंट्स का प्रतिशत है। यदि 100% म्यूटेंट्स मारे गए, तो कोडबेस में अवांछित (unwanted) या आकस्मिक परिवर्तनों का पता लगाने के लिए यूनिट टेस्ट्स पर भरोसा किया जा सकता है।
बहुत बड़े कोडबेस के लिए, 100% स्कोर प्राप्त करना अव्यावहारिक हो सकता है। अधिकांश बैकएंड और फ्रंटएंड एप्लिकेशन जैसे पारंपरिक कोडबेस की तुलना में Solidity स्मार्ट कॉन्ट्रैक्ट्स काफी छोटे होते हैं। इतने बड़े कोडबेस के लिए 100% म्यूटेशन स्कोर का लक्ष्य रखना असंभव हो सकता है। लेकिन क्योंकि Solidity स्मार्ट कॉन्ट्रैक्ट अपेक्षाकृत छोटे होते हैं, और बग्स विनाशकारी (catastrophic) होते हैं, इसलिए बचने वाले म्यूटेंट्स की सावधानीपूर्वक जांच की जानी चाहिए।
म्यूटेशन टेस्टिंग की सीमाएं
क्योंकि म्यूटेशन टेस्टिंग यूनिट टेस्ट्स की गुणवत्ता का परीक्षण करती है, और यूनिट टेस्ट्स आमतौर पर स्टेटलेस (stateless) होते हैं, म्यूटेशन टेस्टिंग स्वाभाविक रूप से यह स्पष्ट नहीं कर सकती कि स्टेटफुल (stateful) बिज़नेस लॉजिक का परीक्षण ठीक से हो रहा है।
म्यूटेशन टेस्टिंग सैकड़ों म्यूटेशन्स बना सकती है, लेकिन समय बचाने के लिए, अधिकांश टूल केवल उनके एक सबसेट (subset) को चलाते हैं। इसका मतलब है कि टेस्ट सूट में बग्स को उजागर करने वाले महत्वपूर्ण म्यूटेशन्स छूट सकते हैं।
और जानें
यह सामग्री हमारे Solidity bootcamp का हिस्सा है। आप हमारे मुफ़्त Solidity course के साथ भी मुफ़्त में Solidity सीख सकते हैं।
मूल रूप से 14 अप्रैल, 2023 को प्रकाशित