यह लेख बताएगा कि Foundry का उपयोग करके Solidity में यूनिट टेस्ट कैसे बनाएं। हम कवर करेंगे कि एक स्मार्ट कॉन्ट्रैक्ट में होने वाले सभी state transitions का परीक्षण कैसे करें, साथ ही Foundry द्वारा प्रदान की जाने वाली कुछ अतिरिक्त उपयोगी विशेषताएं। Foundry में बहुत व्यापक परीक्षण क्षमताएं हैं, इसलिए डॉक्यूमेंटेशन को दोहराने के बजाय, हम उन हिस्सों पर ध्यान केंद्रित करेंगे जिनका आप ज्यादातर समय उपयोग करेंगे।
यह लेख मानकर चलता है कि आप पहले से ही Solidity के साथ सहज हैं। यदि नहीं, तो हमारा मुफ्त learn solidity ट्यूटोरियल देखें।
Foundry इंस्टॉल करें
यदि आपके पास पहले से Foundry इंस्टॉल नहीं है, तो यहां दिए गए निर्देशों का पालन करें: https://book.getfoundry.sh/getting-started/installation
लेखक
इस लेख के सह-लेखक Aymeric Taylor (LinkedIn, Twitter) हैं, जो RareSkills में एक रिसर्च इंटर्न हैं।
Foundry Hello World
बस निम्नलिखित कमांड चलाएँ और यह आपके लिए एनवायरनमेंट सेट अप करेगा, टेस्ट बनाएगा और उन्हें चलाएगा। (बेशक, यह मानकर कि आपके पास Foundry इंस्टॉल है)।
forge init
forge test
Solidity टेस्टिंग बेस्ट प्रैक्टिसेस
फ्रेमवर्क चाहे जो भी हो, Solidity यूनिट टेस्ट्स की गुणवत्ता तीन कारकों पर निर्भर करती है:
- Line coverage
- branch coverage, और
- पूरी तरह से परिभाषित state transitions।
इनमें से प्रत्येक को समझकर, हम यह स्पष्ट कर सकते हैं कि हम Foundry API के कुछ खास पहलुओं पर ध्यान केंद्रित क्यों करते हैं।
निश्चित रूप से, हर संभव आउटपुट के लिए इनपुट की हर रेंज को डॉक्यूमेंट करना संभव नहीं है। हालाँकि, टेस्ट की गुणवत्ता आम तौर पर line coverage, branch coverage और state transitions को परिभाषित करने से सहसंबद्ध (correlated) होगी। हमारे दूसरे लेख में, हमने बताया है कि Foundry के साथ line और branch coverage को कैसे मापें। हम यहाँ इन तीनों मेट्रिक्स के महत्व को समझाएंगे:
1. Line coverage
Line coverage वैसा ही है जैसा इसका नाम है। यदि टेस्ट के दौरान कोड की कोई लाइन निष्पादित (execute) नहीं हुई, तो line coverage 100% नहीं है। यदि कोई लाइन कभी execute ही नहीं हुई, तो आप सुनिश्चित नहीं हो सकते कि यह अपेक्षित रूप से काम करेगी या रिवर्ट (revert) हो जाएगी। स्मार्ट कॉन्ट्रैक्ट में 100% line coverage न होने का कोई उचित कारण नहीं है। यदि आप कोड लिख रहे हैं, तो इसका मतलब है कि आप भविष्य में किसी समय इसके execute होने की उम्मीद करते हैं, तो फिर इसका परीक्षण क्यों न करें?
2. Branch coverage
भले ही हर लाइन execute हो जाए, लेकिन इसका मतलब यह नहीं है कि स्मार्ट कॉन्ट्रैक्ट के बिजनेस लॉजिक में हर बदलाव (variation) का परीक्षण कर लिया गया है।
निम्नलिखित फ़ंक्शन पर विचार करें
function changeOwner(address newOwner) external {
require(msg.sender == owner, "onlyOwner");
owner = newOwner;
}
यदि आप owner के साथ कॉल करके इस एड्रेस का परीक्षण करते हैं, तो आपको 100% line coverage मिलेगा लेकिन 100% branch coverage नहीं। ऐसा इसलिए है क्योंकि require स्टेटमेंट और owner असाइनमेंट दोनों execute हुए, लेकिन वह स्थिति जहाँ require रिवर्ट हुआ, उसका परीक्षण नहीं किया गया।
यहाँ एक अधिक सूक्ष्म उदाहरण दिया गया है।
// @notice anyone can pay off someone else's loan
// @param debtor the person who's loan the sender is making a payment for
function payDownLoan(address debtor) external payable {
uint256 loanAmount = loanAmounts[debtor];
require(loanAmount > 0, "no such loan");
if (msg.value >= debtAmount {
loanAmounts[debtor] = 0;
emit LoanFullyRepaid(debtor);
} else {
emit LoanPayment(debtor, debtAmount, msg.value);
loanAmount -= msg.value;
}
if (msg.value > loanAmount) {
msg.sender.call{value: msg.value - loanAmount}("");
}
}
इस मामले में परीक्षण करने के लिए कितनी ब्रांचेस (branches) हैं?
- वह स्थिति जहाँ लोन शून्य है
- वह स्थिति जहाँ कोई लोन के आकार से कम भुगतान करता है
- वह स्थिति जहाँ कोई ठीक लोन के आकार के बराबर भुगतान करता है
- वह स्थिति जहाँ कोई लोन के आकार से अधिक भुगतान करता है
लोन के आकार से अधिक ईथर (ether) और लोन के आकार से कम ईथर भेजकर इस टेस्ट पर 100% line coverage प्राप्त करना संभव है। यह if else की दोनों ब्रांचेस और अंत में अंतिम if स्टेटमेंट को execute करेगा। लेकिन यह उस else स्टेटमेंट का परीक्षण नहीं करेगा जहाँ लोन का भुगतान पूरी तरह से शून्य कर दिया गया हो।
आपके फ़ंक्शन्स में जितनी अधिक ब्रांचेस होंगी, उनका यूनिट टेस्ट करना उतना ही अधिक कठिन होता जाएगा। इसके लिए तकनीकी शब्द cyclomatic complexity है।
3. पूरी तरह से परिभाषित state transitions
Solidity में गुणवत्तापूर्ण यूनिट टेस्ट्स state transitions को जितना संभव हो उतनी अच्छी तरह से डॉक्यूमेंट करते हैं। State transitions में शामिल हैं:
- स्टोरेज वेरिएबल्स (storage variables) में बदलाव
- कॉन्ट्रैक्ट्स का डिप्लॉय (deploy) या सेल्फ-डिस्ट्रक्ट (self-destruct) होना
- ईथर बैलेंस का बदलना
- कुछ विशिष्ट संदेशों के साथ इवेंट्स (events) का एमिट (emit) होना
- विशिष्ट त्रुटि संदेशों (error messages) के साथ ट्रांज़ैक्शन का रिवर्ट होना
यदि कोई फ़ंक्शन इनमें से कोई भी कार्य करता है, तो उसके द्वारा state को संशोधित करने का सटीक तरीका यूनिट टेस्ट्स में कैप्चर किया जाना चाहिए और किसी भी तरह के विचलन (deviation) के कारण रिवर्ट होना चाहिए। इस तरह, कोई भी आकस्मिक संशोधन, चाहे वह कितना भी छोटा क्यों न हो, स्वचालित रूप से पकड़ लिया जाएगा।
पिछले उदाहरण पर जाते हुए, किन state transitions को मापा जाना चाहिए?
- कॉन्ट्रैक्ट में ईथर उसी मात्रा में बढ़ता है जितना उधारकर्ता (borrower) लोन वापस चुकाता है
- लोन के आकार को ट्रैक करने वाला स्टोरेज वेरिएबल अपेक्षित मात्रा में कम हो जाता है
- जब सेंडर किसी ऐसे लोन के लिए भुगतान करता है जो मौजूद नहीं है, तो अपेक्षित त्रुटि संदेश के साथ रिवर्ट होता है
- संबंधित इवेंट्स और जुड़े हुए संदेश एमिट होते हैं
यदि आपके स्मार्ट कॉन्ट्रैक्ट में बिजनेस लॉजिक बदलता है, तो टेस्ट्स फेल होने चाहिए। आमतौर पर, अन्य डोमेन में इसे एक “fragile” (नाज़ुक) यूनिट टेस्ट माना जाता है। यह सोर्स कोड पर पुनरावृत्ति (iterating) की गति को नुकसान पहुंचा सकता है। लेकिन Solidity कोड एक बार लिखने और कभी न बदलने के लिए बनाया गया है, इसलिए स्मार्ट कॉन्ट्रैक्ट टेस्टिंग के लिए यह कोई समस्या नहीं है।
4. यूनिट टेस्टिंग बेस्ट प्रैक्टिस निष्कर्ष
हम Foundry यूनिट टेस्टिंग कैसे काम करता है, इसे डॉक्यूमेंट करने से पहले इन सब को क्यों कवर कर रहे हैं? क्योंकि यह हमें उन उच्च प्रभाव (high impact) वाली टेस्टिंग यूटिलिटीज को अलग करने में मदद करेगा जिनका आप ज्यादातर समय उपयोग करेंगे। Foundry की क्षमताएं विशाल हैं, लेकिन अधिकांश टेस्ट मामलों में इसके केवल एक छोटे से हिस्से का उपयोग किया जाएगा।
Foundry Asserts
यह सुनिश्चित करने के लिए कि वास्तव में एक state transition हुआ है, आपको asserts की आवश्यकता होगी।
आइए उस डिफ़ॉल्ट टेस्ट फ़ाइल से शुरू करते हैं जो forge init कॉल करने के बाद Foundry प्रदान करता है।
// 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);
}
}
setUp() फ़ंक्शन उस कॉन्ट्रैक्ट को डिप्लॉय करता है जिसका आप परीक्षण कर रहे हैं (साथ ही इकोसिस्टम में कोई भी अन्य कॉन्ट्रैक्ट जो आप चाहते हैं)।
कोई भी फ़ंक्शन जो test शब्द से शुरू होता है, उसे यूनिट टेस्ट के रूप में execute किया जाएगा। जो फ़ंक्शन test से शुरू नहीं होते हैं, वे तब तक execute नहीं होंगे जब तक कि कोई test या setUp फ़ंक्शन उन्हें कॉल न करे।
यहाँ वे asserts दिए गए हैं जो आपके उपयोग के लिए उपलब्ध हैं।
जिनका आप सबसे अधिक उपयोग करेंगे वे हैं
assertEq, assert equal (समान)assertLt, assert less than (से कम)assertLe, assert less than or equal to (से कम या बराबर)assertGt, assert greater than (से अधिक)assertGe, assert greater than or equal to (से अधिक या बराबर)assertTrue, assert to be true (सत्य होने की पुष्टि)
Assert के पहले दो आर्ग्यूमेंट्स तुलना (comparison) के लिए हैं, लेकिन आप तीसरे आर्ग्यूमेंट के रूप में एक उपयोगी त्रुटि संदेश भी जोड़ सकते हैं, जो आपको हमेशा करना चाहिए (भले ही डिफ़ॉल्ट उदाहरण इसे नहीं दिखाता है)। यहाँ assertions लिखने का सुझाया गया तरीका दिया गया है:
function testIncrement() public {
counter.increment();
assertEq(counter.number(), 1, "expect x to equal to 1");
}
function testSetNumber(uint256 x) public {
counter.setNumber(x);
assertEq(counter.number(), x, "x should be setNumber");
}
Foundry vm.prank के साथ msg.sender को बदलना
सेंडर (अकाउंट या वॉलेट) को बदलने का Foundry का काफी हास्यास्पद तरीका vm.prank API है (जिसे Foundry चीटकोड (cheatcode) कहता है)।
यहाँ एक छोटा सा उदाहरण है
function testChangeOwner() public {
vm.prank(owner);
contractToTest.changeOwner(newOwner);
assertEq(contractToTest.owner(), newOwner);
}
vm.prank केवल उसी ट्रांज़ैक्शन के लिए काम करता है जो इसके तुरंत बाद होता है। यदि आप चाहते हैं कि ट्रांज़ैक्शन की एक श्रृंखला (sequence) उसी एड्रेस का उपयोग करे, तो vm.startPrank का उपयोग करें और उन्हें vm.stopPrank के साथ समाप्त करें।
function testMultipleTransactions() public {
vm.startPrank(owner);
// behave as owner
vm.stopPrank();
}
Foundry में अकाउंट्स और एड्रेसेस को परिभाषित करना
ऊपर दिए गए owner वेरिएबल को कुछ तरीकों से परिभाषित किया जा सकता है:
// an address created by casting a decimal to an address
address owner = address(1234);
// vitalik's addresss
address owner = 0x0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045;
// create an address from a known private key;
address owner = vm.addr(privateKey);
// create an attacker
address hacker = 0x00baddad
msg.sender और tx.origin प्रैंक (prank)
उपरोक्त उदाहरणों में, msg.sender को बदल दिया गया है। यदि आप विशेष रूप से tx.origin और msg.sender दोनों पर नियंत्रण चाहते हैं, तो vm.prank और vm.startPrank दोनों वैकल्पिक रूप से दो आर्ग्यूमेंट्स लेते हैं जहाँ दूसरा आर्ग्यूमेंट tx.origin होता है।
vm.prank(msgSender, txOrigin);
tx.origin पर निर्भर रहना आमतौर पर एक गलत अभ्यास (bad practice) माना जाता है, इसलिए आपको शायद ही कभी vm.prank के दो-आर्ग्यूमेंट संस्करण का उपयोग करने की आवश्यकता होगी।
बैलेंस चेक करना
जब आप ईथर ट्रांसफर करते हैं, तो आपको यह मापना चाहिए कि बैलेंस अपेक्षित रूप से बदल गया है। शुक्र है, Foundry में बैलेंस चेक करना आसान है, क्योंकि यह Solidity में लिखा गया है।
इस कॉन्ट्रैक्ट पर विचार करें:
contract Deposit {
event Deposited(address indexed);
function buyerDeposit() external payable {
require(msg.value == 1 ether, "incorrect amount");
emit Deposited(msg.sender);
}
// rest of the logic
}
टेस्ट फ़ंक्शन कुछ इस तरह दिखेगा।
function testBuyerDeposit() public {
uint256 balanceBefore = address(depositContract).balance;
depositContract.buyerDeposit{value: 1 ether}();
uint256 balanceAfter = address(depositContract).balance;
assertEq(balanceAfter - balanceBefore, 1 ether, "expect increase of 1 ether");
}
ध्यान दें कि हमने उन मामलों का परीक्षण नहीं किया है जहाँ खरीदार ने 1 ईथर के अलावा कोई अन्य राशि भेजी हो, जिसके कारण रिवर्ट हो सकता है। हम अगले अनुभाग में रिवर्ट्स के परीक्षण पर चर्चा करेंगे।
vm.expectRevert के साथ रिवर्ट्स की अपेक्षा करना
अपने वर्तमान स्वरूप में उपरोक्त टेस्ट के साथ समस्या यह है कि आप require स्टेटमेंट को हटा सकते हैं और टेस्ट फिर भी पास हो जाएगा। आइए टेस्ट में सुधार करें ताकि require स्टेटमेंट को हटाने से टेस्ट फेल हो जाए।
function testBuyerDepositWrongPrice() public {
vm.expectRevert("incorrect amount");
depositContract.deposit{value: 1 ether + 1 wei}();
vm.expectRevert("incorrect amount");
depositContract.deposit{value: 1 ether - 1 wei}();
}
ध्यान दें कि जिस फ़ंक्शन के रिवर्ट होने की हमें उम्मीद है, उसे करने से ठीक पहले vm.expectRevert को कॉल किया जाना चाहिए। अब यदि हम require स्टेटमेंट को हटा देते हैं, तो यह रिवर्ट हो जाएगा, इसलिए हमने स्मार्ट कॉन्ट्रैक्ट की इच्छित कार्यक्षमता को बेहतर ढंग से मॉडल किया है।
कस्टम एरर्स (Custom Errors) की टेस्टिंग
यदि हम require स्टेटमेंट्स के बजाय कस्टम एरर्स (custom errors) का उपयोग करते हैं, तो रिवर्ट का परीक्षण करने का तरीका इस प्रकार होगा:
contract CustomErrorContract {
error SomeError(uint256);
function revertError(uint256 x) public pure {
revert SomeError(x);
}
}
और टेस्ट फ़ाइल इस तरह होगी
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/RevertCustomError.sol";
contract CounterTest is Test {
CustomErrorContract public customErrorContract;
error SomeError(uint256);
function setUp() public {
customErrorContract = new CustomErrorContract();
}
function testRevert() public {
// 5 is an arbitrary example
vm.expectRevert(abi.encodeWithSelector(SomeError.selector, 5));
customErrorContract.revertError(5);
}
}
हमारे उदाहरण में, हमने एक पैरामीटराइज़्ड कस्टम एरर बनाया है। टेस्ट पास होने के लिए, पैरामीटर उस पैरामीटर के बराबर होना चाहिए जो वास्तव में रिवर्ट के दौरान उपयोग किया गया था।
vm.expectEvent के साथ लॉग्स और इवेंट्स की टेस्टिंग
हालाँकि Solidity इवेंट्स एक स्मार्ट कॉन्ट्रैक्ट की कार्यक्षमता को नहीं बदलते हैं, लेकिन उन्हें गलत तरीके से लागू करने से क्लाइंट एप्लिकेशन टूट सकते हैं जो एक स्मार्ट कॉन्ट्रैक्ट के state को पढ़ते हैं। यह सुनिश्चित करने के लिए कि हमारे इवेंट्स अपेक्षित रूप से काम करते हैं, हम vm.expectEmit का उपयोग कर सकते हैं। यह API थोड़ा अप्रत्याशित (counterintuitively) रूप से व्यवहार करता है क्योंकि आपको टेस्ट में इवेंट को एमिट करना होगा ताकि यह सुनिश्चित हो सके कि यह स्मार्ट कॉन्ट्रैक्ट में काम कर रहा है।
यहाँ एक छोटा सा उदाहरण है।
function testBuyerDepositEvent() public {
vm.expectEmit();
emit Deposited(buyer);
depositContract.deposit{value: 1 ether}();
}
vm.warp के साथ block.timestamp को एडजस्ट करना
अब आइए एक टाइम लॉक्ड विड्रॉल (time locked withdrawal) पर विचार करें। विक्रेता (seller) 3 दिनों के बाद पेमेंट निकाल सकता है।
contract Deposit {
address public seller;
mapping(address => uint256) public depositTime;
event Deposited(address indexed);
event SellerWithdraw(address indexed, uint256 indexed);
constructor(address _seller) {
seller = _seller;
}
function buyerDeposit() external payable {
require(msg.value == 1 ether, "incorrect amount");
uint256 _depositTime = depositTime[msg.sender];
require(_depositTime == 0, "already deposited");
depositTime[msg.sender] = block.timestamp;
emit Deposited(msg.sender);
}
function sellerWithdraw(address buyer) external {
require(msg.sender == seller, "not the seller");
uint256 _depositTime = depositTime[buyer];
require(_depositTime != 0, "buyer did not deposit");
require(block.timestamp - _depositTime > 3 days, "refund period not passed");
delete depositTime[buyer];
emit SellerWithdraw(buyer, block.timestamp);
(bool ok, ) = msg.sender.call{value: 1 ether}("");
require(ok, "seller did not withdraw");
}
}
हमने बहुत सी कार्यक्षमता जोड़ी है जिसका परीक्षण करने की आवश्यकता है, लेकिन अभी के लिए समय के पहलू पर ध्यान केंद्रित करते हैं।
हम यह परीक्षण करना चाहते हैं कि विक्रेता डिपॉजिट के बाद 3 दिन पूरे होने तक पैसे नहीं निकाल सकता है। (स्पष्ट रूप से खरीदार के लिए उस विंडो से पहले पैसे निकालने का एक फ़ंक्शन गायब है, लेकिन हम उस पर बाद में आएंगे)।
ध्यान दें कि block.timestamp डिफ़ॉल्ट रूप से 1 से शुरू होता है। यह परीक्षण करने के लिए एक यथार्थवादी (realistic) संख्या नहीं है, इसलिए हमें पहले वर्तमान दिन पर जाना (warp) चाहिए।
यह vm.warp(x) के साथ किया जा सकता है, लेकिन आइए थोड़ा एडवांस (fancy) बनें और एक मॉडिफायर (modifier) का उपयोग करें।
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/Deposit.sol";
contract DepositTest is Test {
Deposit public deposit;
Deposit public faildeposit;
address constant SELLER = address(0x5E11E7);
//address constant Rejector = address(RejectTransaction);
RejectTransaction private rejector;
event Deposited(address indexed);
event SellerWithdraw(address indexed, uint256 indexed);
function setUp() public {
deposit = new Deposit(SELLER);
rejector = new RejectTransaction();
faildeposit = new Deposit(address(rejector));
}
modifier startAtPresentDay() {
vm.warp(1680616584);
_;
}
address public buyer = address(this); // the DepositTest contract is the "buyer"
address public buyer2 = address(0x5E11E1); // random address
address public FakeSELLER = address(0x5E1222); // random address
function testDepositAmount() public startAtPresentDay {
// this test checks that the buyer can only deposit 1 ether
vm.startPrank(buyer);
vm.expectRevert();
deposit.buyerDeposit{value: 1.5 ether}();
vm.expectRevert();
deposit.buyerDeposit{value: 2.5 ether}();
vm.stopPrank();
}
}
vm.roll के साथ block.number को एडजस्ट करना
यदि आप Foundry में ब्लॉक नंबर (block.number) को एडजस्ट करना चाहते हैं, तो इसका उपयोग करें
vm.roll(blockNumber)
ब्लॉक नंबर बदलने के लिए। एक निश्चित संख्या में ब्लॉक्स को आगे बढ़ाने के लिए, निम्नलिखित करें
vm.roll(block.number() + numberOfBlocks)
अतिरिक्त टेस्ट्स जोड़ना
पूर्णता के लिए, आइए बाकी फ़ंक्शन्स के लिए यूनिट टेस्ट्स लिखें।
डिपॉजिट फ़ंक्शन के लिए कुछ अतिरिक्त विशेषताओं का परीक्षण करने की आवश्यकता है:
- पब्लिक वेरिएबल
depositTimeट्रांज़ैक्शन के समय से मेल खाता है - कोई यूज़र दो बार डिपॉजिट नहीं कर सकता
और सेलर फ़ंक्शन के लिए:
- विक्रेता गैर-मौजूद (non-existent) एड्रेसेस के लिए पैसे नहीं निकाल सकता
- खरीदार के लिए एंट्री हटा दी जाती है (यह खरीदार को फिर से खरीदने की अनुमति देता है)
SellerWithdrawइवेंट एमिट होता है- कॉन्ट्रैक्ट का बैलेंस 1 ईथर कम हो जाता है
- एक ऐसा एड्रेस जो विक्रेता नहीं है और
sellerWithdrawको कॉल करता है, वह रिवर्ट हो जाता है
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/Deposit.sol";
contract DepositTest is Test {
Deposit public deposit;
Deposit public faildeposit;
address constant SELLER = address(0x5E11E7);
//address constant Rejector = address(RejectTransaction);
RejectTransaction private rejector;
event Deposited(address indexed);
event SellerWithdraw(address indexed, uint256 indexed);
function setUp() public {
deposit = new Deposit(SELLER);
rejector = new RejectTransaction();
faildeposit = new Deposit(address(rejector));
}
modifier startAtPresentDay() {
vm.warp(1680616584);
_;
}
address public buyer = address(this); // the DepositTest contract is the "buyer"
address public buyer2 = address(0x5E11E1); // random address
address public FakeSELLER = address(0x5E1222); // random address
function testDepositAmount() public startAtPresentDay {
// this test checks that the buyer can only deposit 1 ether
vm.startPrank(buyer);
vm.expectRevert();
deposit.buyerDeposit{value: 1.5 ether}();
vm.expectRevert();
deposit.buyerDeposit{value: 2.5 ether}();
vm.stopPrank();
}
function testBuyerDepositSellerWithdrawAfter3days() public startAtPresentDay {
// This test checks that the seller is able to withdraw 3 days after the buyer deposits
// buyer deposits 1 ether
vm.startPrank(buyer); // msg.sender == buyer
deposit.buyerDeposit{value: 1 ether}();
assertEq(address(deposit).balance, 1 ether, "Contract balance did not increase"); // checks to see if the contract balance increases
vm.stopPrank();
// after three days the seller withdraws
vm.startPrank(SELLER); // msg.sender == SELLER
vm.warp(1680616584 + 3 days + 1 seconds);
deposit.sellerWithdraw(address(this));
assertEq(address(deposit).balance, 0 ether, "Contract balance did not decrease"); // checks to see if the contract balance decreases
}
function testBuyerDepositSellerWithdrawBefore3days() public startAtPresentDay {
// This test checks that the seller is able to withdraw 3 days after the buyer deposits
// buyer deposits 1 ether
vm.startPrank(buyer); // msg.sender == buyer
deposit.buyerDeposit{value: 1 ether}();
assertEq(address(deposit).balance, 1 ether, "Contract balance did not increase"); // checks to see if the contract balance increases
vm.stopPrank();
// before three days the seller withdraws
vm.startPrank(SELLER); // msg.sender == SELLER
vm.warp(1680616584 + 2 days);
vm.expectRevert(); // expects a revert
deposit.sellerWithdraw(address(this));
}
function testdepositTimeMatchesTimeofTransaction() public startAtPresentDay {
// This test checks that the public variable depositTime matches the time of the transaction
vm.startPrank(buyer); // msg.sender == buyer
deposit.buyerDeposit{value: 1 ether}();
// check that it deposits at the right time
assertEq(
deposit.depositTime(buyer),
1680616584, // time of startAtPresentDay
"Time of Deposit Doesnt Match"
);
vm.stopPrank();
}
function testUserDepositTwice() public startAtPresentDay {
// This test checks that a user cannot deposit twice
vm.startPrank(buyer); // msg.sender == buyer
deposit.buyerDeposit{value: 1 ether}();
vm.warp(1680616584 + 1 days); // one day later...
vm.expectRevert();
deposit.buyerDeposit{value: 1 ether}(); // should revert since it hasn't been 3 days
}
function testNonExistantContract() public startAtPresentDay {
// This test checks that the seller cannot withdraw for non-existent addresses
vm.startPrank(SELLER); // msg.sender == SELLER
vm.expectRevert();
deposit.sellerWithdraw(buyer);
}
function testBuyerBuysAgain() public startAtPresentDay {
// This test checks that the entry for the buyer is deleted (this allows the buyer to buy again)
vm.startPrank(buyer); // msg.sender == buyer
deposit.buyerDeposit{value: 1 ether}();
vm.stopPrank();
// seller withdraws
vm.warp(1680616584 + 3 days + 1 seconds);
vm.startPrank(SELLER); // msg.sender == SELLER
deposit.sellerWithdraw(buyer);
vm.stopPrank();
// checks depostitime[buyer] == 0
assertEq(deposit.depositTime(buyer), 0, "entry for buyer is not deleted");
// buyer deposits again
vm.startPrank(buyer); // msg.sender == buyer
vm.expectEmit();
emit Deposited(buyer);
deposit.buyerDeposit{value: 1 ether}();
vm.stopPrank();
}
function testSellerWithdrawEmitted() public startAtPresentDay {
// this test checks that the SellerWithdraw event is emitted
//buyer2 deposits
vm.deal(buyer2, 1 ether); // msg.sender == buyer2
vm.startPrank(buyer2);
vm.expectEmit(); // Deposited Emitter checked
emit Deposited(buyer2);
deposit.buyerDeposit{value: 1 ether}();
vm.stopPrank();
vm.warp(1680616584 + 3 days + 1 seconds);// 3 day and 1 second later...
// seller withdraws + checks SellerWithdraw event emmited or not
vm.startPrank(SELLER); // msg.sender == SELLER
vm.expectEmit(); // expects SellerWithdraw Emitterd
emit SellerWithdraw(buyer2, block.timestamp);
deposit.sellerWithdraw(buyer2);
vm.stopPrank();
}
function testFakeSeller2Withdraw() public startAtPresentDay {
// buyer deposits
vm.startPrank(buyer);
vm.deal(buyer, 2 ether); // this contract's address is the buyer
deposit.buyerDeposit{value: 1 ether}();
vm.stopPrank();
assertEq(address(deposit).balance, 1 ether, "Ether deposited somehow failed");
vm.warp(1680616584 + 3 days + 1 seconds); // 3 day and 1 second later...
vm.startPrank(FakeSELLER); // msg.sender == FakeSELLER
vm.expectRevert();
deposit.sellerWithdraw(buyer);
vm.stopPrank();
}
function testRejectedWithdrawl() public startAtPresentDay {
// This test checks that the entry for the buyer is deleted (this allows the buyer to buy again)
vm.startPrank(buyer); // msg.sender == buyer
faildeposit.buyerDeposit{value: 1 ether}();
vm.stopPrank();
assertEq(address(faildeposit).balance, 1 ether, "assertion failed");
vm.warp(1680616584 + 3 days + 1 seconds); // 3 days and 1 second later...
vm.startPrank(address(rejector)); // msg.sender == rejector
vm.expectRevert();
faildeposit.sellerWithdraw(buyer);
vm.stopPrank();
}
}
फेल हुए ईथर ट्रांसफर्स की टेस्टिंग
खरीदार के विड्रॉल (withdraw) का परीक्षण करने के लिए फुल line coverage प्राप्त करने हेतु एक अतिरिक्त ट्रिक की आवश्यकता होती है। यहाँ वह स्निपेट है जिसका हम परीक्षण कर रहे हैं, और हम ऊपर दिए गए कोड में Rejector कॉन्ट्रैक्ट की व्याख्या करेंगे।
function buyerWithdraw() external {
uint256 _depositTime = depositTime[msg.sender];
require(_depositTime != 0, "sender did not deposit");
require(block.timestamp - _depositTime <= 3 days);
emit BuyerRefunded(msg.sender, block.timestamp);
// this is the branch we are testing
(bool ok,) = msg.sender.call{value: 1 ether}("");
require(ok, "Failed to withdraw");
}
require(ok…) की फेल स्थिति का परीक्षण करने के लिए हमें ईथर ट्रांसफर के फेल होने की आवश्यकता है। टेस्ट इसे एक स्मार्ट कॉन्ट्रैक्ट बनाकर पूरा करता है जो buyerWithdraw फ़ंक्शन को कॉल करता है, लेकिन इसका receive फ़ंक्शन revert पर सेट होता है।
Foundry फ़ज़िंग (Fuzzing)
हालाँकि हम अनधिकृत (unauthorized) एड्रेस द्वारा विड्रॉल करने पर होने वाले revert का परीक्षण करने के लिए एक ऐसा मनमाना (arbitrary) एड्रेस निर्दिष्ट कर सकते हैं जो विक्रेता का नहीं है, फिर भी बहुत सारे अलग-अलग मानों (values) को आज़माना मानसिक रूप से अधिक आश्वस्त करने वाला होता है।
यदि हम टेस्ट फ़ंक्शन्स में कोई आर्ग्यूमेंट प्रदान करते हैं, तो Foundry आर्ग्यूमेंट्स के लिए कई अलग-अलग मानों को आज़माएगा। इसे ऐसे आर्ग्यूमेंट्स का उपयोग करने से रोकने के लिए जो टेस्ट केस पर लागू नहीं होते हैं (जैसे कि जब एड्रेस अधिकृत हो), तो हम vm.assume का उपयोग करेंगे। यहाँ बताया गया है कि हम एक अनधिकृत विक्रेता के लिए सेलर विड्रॉल का परीक्षण कैसे कर सकते हैं।
// notSeller will be chosen randomly
function testInvalidSellerAddress(address notSeller) public {
vm.assume(notSeller != seller);
vm.expectRevert("not the seller");
depositContract.sellerWithdraw(notSeller);
}
यहाँ सभी state transitions दिए गए हैं
- कॉन्ट्रैक्ट का
balance1 ईथर से कम हो जाता है BuyerRefundedइवेंट एमिट हुआ था- खरीदार तीन दिन से पहले रिफंड ले सकता है
यहाँ वे ब्रांचेस दी गई हैं जिनका परीक्षण करने की आवश्यकता है
- खरीदार 3 दिनों के बाद पैसे नहीं निकाल सकता
- खरीदार पैसे नहीं निकाल सकता यदि उसने कभी डिपॉजिट नहीं किया
Foundry में Console.log
Foundry में console.log करने के लिए, निम्नलिखित को इम्पोर्ट करें
import "forge-std/console.sol";
और इसके साथ टेस्ट चलाएँ
forge test -vv
सिगनेचर्स (Signatures) की टेस्टिंग
Foundry के साथ Solidity सिगनेचर वेरिफिकेशन (signature verification) पर हमारा ट्यूटोरियल देखें, इसलिए हम आपको उसका संदर्भ (refer) देते हैं।
Solidity में इंटरनल फ़ंक्शन्स (internal functions) का टेस्ट
Solidity में इंटरनल फ़ंक्शन्स की टेस्टिंग पर हमारा ट्यूटोरियल देखें।
vm.deal और vm.hoax के साथ एड्रेस बैलेंस सेट करना
चीट कोड vm.hoax आपको एक ही समय में एक एड्रेस को प्रैंक (prank) करने और उसका बैलेंस सेट करने की अनुमति देता है।
vm.hoax(addressToPrank, balanceToGive);
// next call is a prank for addressToPrank
vm.deal(alice, balanceToGive);
Foundry के साथ कुछ सामान्य गलतियाँ
ईथर प्राप्त करते समय फ़ॉलबैक (fallback) फ़ंक्शन का न होना
यदि आप कॉन्ट्रैक्ट से ईथर निकालने (withdrawing) का परीक्षण कर रहे हैं, तो इसे उस कॉन्ट्रैक्ट में भेजा जाएगा जो टेस्ट चला रहा है। Foundry टेस्ट स्वयं एक स्मार्ट कॉन्ट्रैक्ट होते हैं, और यदि आप ऐसे स्मार्ट कॉन्ट्रैक्ट को ईथर भेजते हैं जिसमें fallback या receive फ़ंक्शन नहीं है, तो ट्रांज़ैक्शन फेल हो जाएगा। सुनिश्चित करें कि कॉन्ट्रैक्ट में एक fallback या receive फ़ंक्शन हो।
टोकन प्राप्त करते समय onERC…Received का न होना
इसी तरह (by the same token), ERC-721 safeTransferFrom और ERC-1155 transferFrom तब रिवर्ट हो जाते हैं जब किसी ऐसे स्मार्ट कॉन्ट्रैक्ट में टोकन भेजे जाते हैं जिसमें उपयुक्त ट्रांसफर हुक फ़ंक्शन (transfer hook function) नहीं होता है। यदि आप स्वयं को NFTs (या ERC777-जैसे टोकन) ट्रांसफर करने का परीक्षण करना चाहते हैं तो आपको इसे अपने टेस्ट्स में जोड़ना होगा।
सारांश
- 100% line और branch coverage का लक्ष्य रखें
- अपेक्षित state transitions को पूरी तरह से परिभाषित करें
- अपने asserts में त्रुटि संदेशों (error messages) का उपयोग करें
टेस्टिंग के बारे में अधिक जानें
यूनिट टेस्ट्स और बेसिक फ़ज़िंग के अलावा एडवांस Solidity टेस्टिंग सीखने के लिए, कृपया हमारा एडवांस Solidity Bootcamp देखें।
मूल रूप से 11 अप्रैल, 2023 को प्रकाशित