Basics
Test được viết bằng Solidity và theo quy ước thì ta sẽ đặt nó trong thư mục test/.
Ví dụ một file test:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import {Test, console} from "forge-std/Test.sol";
import {FundMe} from "../src/Fundme.sol";
contract FundMeTest is Test {
FundMe private fundMe;
function setUp() external {
fundMe = new FundMe();
console.log(fundMe.i_owner());
}
function testMinimumDollarIsFive() public view {
assertEq(fundMe.MINIMUM_USD(), 5e18);
}
function testOwnerIsMsgSender() public {
assertEq(fundMe.i_owner(), address(this));
}
}Với:
import {Test, console} from "forge-std/Test.sol";là để import thư viện test của Foundry. Ta sẽ kế thừa smart contractTestđể có thể sử dụng các hàm test của Foundry.consolelà một function của Foundry giúp in ra khi chạy lệnhforge testvới flag-vv.
Hàm setUp là một optional function sẽ được thực thi mỗi khi chạy một hàm test. Trong hàm này, ta thường thực hiện những tác vụ cần thiết trước khi test chẳng hạn như deploy contract, khởi tạo giá trị cho các biến, …
Important
Cần chú ý rằng contract
FundMetrong test trên sẽ được deploy mởiFundMeTestcontract chứ không phải bởimsg.sendercủaFundMeTestcontract. Do đó, việc kiểm trafundMe.i_owner() == msg.sendertrong hàmtestOwnerIsMsgSendersẽ bị fail.
Chạy test với lệnh sau:
forge testLệnh này sẽ tìm kiếm tất cả các public/external function bắt đầu bằng tiền tố test để thực thi.
Output của lệnh trên:
Ran 1 test for test/FundMeTest.t.sol:FundMeTest
[PASS] testDemo() (gas: 13660)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 690.40µs (144.90µs CPU time)
Ran 1 test suite in 11.25ms (690.40µs CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)Seealso
Để chỉ thực thi một hàm test nào đó trong file test, ta sẽ specify tên hàm thông qua flag --mt (match test):
forge test --mt testOwnerIsMsgSenderHoặc specify tên test contract:
forge test FundMeTestRunning Tests On Chain Forks
Xét hàm sau:
function getVersion() public view returns (uint256) {
AggregatorV3Interface priceFeed = AggregatorV3Interface(
0x694AA1769357215DE4FAC081bf1f309aDC325306
);
return priceFeed.version();
}Hàm này gán cứng địa chỉ của Sepolia testnet. Tất nhiên, khi chạy test sau ở trên Anvil local network thì chúng ta sẽ gặp lỗi.
Để giải quyết vấn đề này, ta cần sử dụng thêm flag --fork-url để chạy một Anvil instance có trạng thái được sao chép y hệt từ Sepolia thông qua RPC URL.
forge test --mt testPriceFeedVersionIsAccurate --fork-url $SEPOLIA_RPC_URLVới $SEPOLIA_RPC_URL là biến môi trường lưu RPC URL kèm API Key của NaaS node.
Important
Nếu có sử dụng
vm.startBroadcast()khi chạy test trên chain fork thì nhớ truyền vào private key/public key của địa chỉ mà ta muốn dùng để deploy các contract. Xem thêm [[#Cheatcodes#startBroadcastandstopBroadcast]].
Coverage
Chúng ta có thể dùng subcommand coverage để hiển thị phần nào trong code được test và phần nào không được test:
forge coverage
Ran 10 tests for test/unit/FundMeTest.t.sol:FundMeTest
[PASS] testAddsFunderToArrayOfFunders() (gas: 103866)
[PASS] testFundFailsWIthoutEnoughETH() (gas: 25124)
[PASS] testFundUpdatesFundDataStructure() (gas: 103190)
[PASS] testMinimumDollarIsFive() (gas: 8885)
[PASS] testOnlyOwnerCanWithdraw() (gas: 102824)
[PASS] testOwnerIsMsgSender() (gas: 9059)
[PASS] testPriceFeedVersionIsAccurate() (gas: 14409)
[PASS] testPrintStorageData() (gas: 18487)
[PASS] testWithdrawFromASingleFunder() (gas: 92703)
[PASS] testWithdrawFromMultipleFunders() (gas: 558620)
Suite result: ok. 10 passed; 0 failed; 0 skipped; finished in 3.31ms (12.39ms CPU time)
Ran 1 test suite in 14.33ms (3.31ms CPU time): 10 tests passed, 0 failed, 0 skipped (10 total tests)
╭---------------------------------+----------------+----------------+---------------+----------------╮
| File | % Lines | % Statements | % Branches | % Funcs |
+====================================================================================================+
| script/DeployFundMe.s.sol | 100.00% (7/7) | 100.00% (9/9) | 100.00% (0/0) | 100.00% (1/1) |
|---------------------------------+----------------+----------------+---------------+----------------|
| script/HelperConfig.s.sol | 66.67% (10/15) | 66.67% (8/12) | 33.33% (1/3) | 66.67% (2/3) |
|---------------------------------+----------------+----------------+---------------+----------------|
| src/FundMe.sol | 72.09% (31/43) | 68.57% (24/35) | 57.14% (4/7) | 83.33% (10/12) |
|---------------------------------+----------------+----------------+---------------+----------------|
| src/PriceConverter.sol | 100.00% (7/7) | 100.00% (8/8) | 100.00% (0/0) | 100.00% (2/2) |
|---------------------------------+----------------+----------------+---------------+----------------|
| test/mocks/MockV3Aggregator.sol | 52.17% (12/23) | 52.94% (9/17) | 100.00% (0/0) | 50.00% (3/6) |
|---------------------------------+----------------+----------------+---------------+----------------|
| Total | 70.53% (67/95) | 71.60% (58/81) | 50.00% (5/10) | 75.00% (18/24) |
╰---------------------------------+----------------+----------------+---------------+----------------╯Tip
Việc kiểm tra coverage cho ta biết rằng ta cần viết thêm hàm test nào.
Chúng ta có thể thêm flag --report debug để xuất báo cáo của lệnh forge coverage giúp cho biết dòng nào trong các smart contract chưa được covered bởi test.
Cheatcodes
Common
Foundry hỗ trợ một số cheat codes dùng để hỗ trợ cho việc test:
-
prank: cho phép gánmsg.senderthành một địa chỉ cụ thể trong các lần call tiếp theo, bao gồm cảstaticcall. -
startPrankvàstopPrank: cho phép gánmsg.senderthành một địa chỉ cụ thể cho tất cả các call trước khistopPrankđược gọi. Tất cả các transaction xảy ra giữastartPrankvàstopPrankđều được ký bởi địa chỉ mà ta chỉ định. -
makeAddr: tạo một địa chỉ mới với tên có dạng chuỗi.address alice = makeAddr("alice");Ví dụ sử dụng các cheatcode trên:
function testFundUpdatesFundDataStructure() public { vm.prank(alice); fundMe.fund{value: SEND_VALUE}(); uint256 amountFunded = fundMe.getAddressToAmountFunded(alice); assertEq(amountFunded, SEND_VALUE); } -
deal: cho phép thiết lập balance của một user. Sử dụng như sau:vm.deal(alice, STARTING_BALANCE);
Nếu dùng nhiều cheat code liền kề nhau thì chúng sẽ không ảnh hưởng lên nhau mà chỉ ảnh hưởng đến transaction:
function testOnlyOwnerCanWithdraw() public {
vm.prank(alice);
fundMe.fund{value: SEND_VALUE}();
vm.expectRevert();
vm.prank(alice);
fundMe.withdraw();
}Trong ví dụ trên, do alice không phải là owner nên transaction sẽ bị revert và vm.expectRevert sẽ chỉ ảnh hưởng đến fundMe.withdraw();.
startBroadcast And stopBroadcast
Chạy các operation trong một transaction mà có thể sign. Thường được dùng để thực hiện việc deploy contract.
Ví dụ:
vm.startBroadcast();
Raffle raffle = new Raffle(
entranceFee,
interval,
vrfCoordinator,
gasLane,
subscriptionId,
callbackGasLimit
);
vm.stopBroadcast();Hàm vm.startBroadcast có thể nhận vào một private key hoặc nhận vào một địa chỉ (public key) để chỉ định tài khoản gửi transaction. Đối với Anvil, ta có thể địa chỉ của default sender account ở đường dẫn forge-std/src/Base.sol.
expectRevert
Đối với cheatcode expectRevert, chúng ta có thể truyền vào selector của error để kiểm tra rằng revert xảy ra với đúng error mà ta mong muốn. Ví dụ ta có error và contract như sau:
error Contract_Error();
contract Contract {}Sử dụng expectRevert như sau:
vm.expectRevert(Contract.Contract_Error.selector);Đối với trường hợp error có signature với tham số như sau:
error Raffle__UpkeepNotNeeded(
uint256 currentBalance,
uint256 numPlayers,
uint256 raffleState
);Ta cần tạo ra selector để truyền vào vm.expectRevert bằng cách sử dụng hàm abi.encodeWithSelector để truyền các đối số vào error:
abi.encodeWithSelector(
Raffle.Raffle__UpkeepNotNeeded.selector,
currentBalance,
numPlayers,
raffleState
)expectPartialRevert
Đôi khi các custom errors có các đối số khó tính toán trong môi trường testing (chẳng hạn giá trị của một internal function trong một third-party contract). Khi đó, chúng ta có thể sử dụng expectPartialRevert để ignore các đối số.
Seealso
expectEmit
Để kiểm tra xem function có emit event hay không, ta sử dụng expectEmit. Ví dụ:
function testEmitsEventOnEntrance() public {
// Arrange
vm.prank(PLAYER);
// Act / Assert
vm.expectEmit(true, false, false, false, address(raffle));
emit EnteredRaffle(PLAYER);
raffle.enterRaffle{value: entranceFee}();
}Theo tài liệu, cheatcode này có các tham số như sau:
function expectEmit(
bool checkTopic1,
bool checkTopic2,
bool checkTopic3,
bool checkData,
address emitter
) external;Với các tham số từ 1 đến 3 là để chứa các indexed parameter của event và checkData là chứa các unindexed parameter của event. Tham số cuối cùng là địa chỉ emit event.
Sau khi gọi cheatcode, ta cần emit event một cách thủ công. Chúng ta có thể truy cập đến event thông qua contract hoặc tự định nghĩa event có cùng signature trong test contract.
warp And roll
Để thay đổi các giá trị liên quan đến block chẳng hạn như block.timestamp hay block.number, ta có thể dùng vm.warp hay vm.roll. Ngoài ra, còn có skip và rewind dùng để skip hoặc rewind block.timestamp một khoảng thời gian cụ thể.
Ví dụ sử dụng:
function testDontAllowPlayersToEnterWhileRaffleIsCalculating() public {
// Arrange
vm.prank(PLAYER);
raffle.enterRaffle{value: entranceFee}();
vm.warp(block.timestamp + interval + 1);
vm.roll(block.number + 1);
raffle.performUpkeep("");
// Act / Assert
vm.expectRevert(Raffle.Raffle__RaffleNotOpen.selector);
vm.prank(PLAYER);
raffle.enterRaffle{value: entranceFee}();
}Hàm test trên là để đảm bảo không có một player nào khác được phép tham gia vào trò chơi của smart contract nếu như hàm performUpkeep đang thực hiện và trạng thái của contract chưa thay đổi thành OPEN. Do hàm performUpkeep chỉ được thực thi khi đã trải qua khoảng thời gian interval nhất định nên ta sử dụng vm.wrap để tua thời gian. Ngoài ra, để giả lập việc blockchain được mined, ta sử dụng hàm vm.roll để tăng block.number.
Events
Chúng ta sẽ dùng cheatcode vm.recordLogs để ghi lại tất cả các event được emit ra vào trong một mảng. Để truy cập, ta sử dụng cheatcode vm.getRecordedLogs.
Do quá trình chạy smart contract có thể có nhiều event được emit ra nên ta sẽ không biết chính xác vị trí của event mà ta muốn kiểm tra ở trong mảng trả về của getRecordedLogs (có kiểu Vm.Log[] memory). Thông thường, ta sẽ sử dụng Debugger để tìm kiếm index của event. Sau khi biết index của event thì ta có thể lấy ra các topic cũng bằng cách sử dụng index. Ví dụ:
vm.recordLogs();
raffle.performUpkeep(""); // emits requestId
Vm.Log[] memory entries = vm.getRecordedLogs();
bytes32 requestId = entries[1].topics[1];Lý do mà ta không sử dụng expectEmit là vì cheatcode này kiểm tra xem có đối số truyền vào một tham số nào đó của event hay không chứ không cho phép kiểm tra xem đối số truyền vào đó có giá trị như thế nào. Mà trong trường hợp này ta lại cần lấy ra requestId thuộc event đã được emit.
envUint
Hàm này dùng để nạp một biến môi trường từ file .env. Ví dụ:
deployerKey: vm.envUint("SEPOLIA_PRIVATE_KEY")readFile
Hàm này cho phép đọc nội dung của file và có signature như sau:
// Reads the entire content of file to string, (path) => (data)
function readFile(string calldata) external returns (string memory);Tuy nhiên, để có thể đọc file ở một path nào đó, ta cần cấp quyền cho nó ở trong file cấu hình foundry.toml:
fs_permissions = [
{ access = "read", path = "./broadcast" },
{ access = "read", path = "./reports" },
{ access = "read", path = "./img/" }
]assertApproxEqAbs
Đây là một hàm assert gần đúng với signature như sau:
function assertApproxEqAbs(uint256 left, uint256 right, uint256 maxDelta) internal;Hàm này cho phép bỏ qua sự khác biệt giữa left và right nếu nó nhỏ hơn maxDelta, đặc biệt thường xảy ra trong các phép chia mà làm mất phần dư.
AAA Pattern
Khi viết test, ta sẽ follow theo AAA pattern: Arange-Act-Assert (cách làm này giống với Check-Effect-Interact pattern, xem thêm Smart Contracts - Security Patterns in the Ethereum Ecosystem and Solidity). Với:
- Arrange: khai báo các biến cần cho việc test
- Act: thực hiện việc test chẳng hạn như gọi hàm.
- Assert: kiểm tra các kết quả của việc test.
Ví dụ:
function testWithdrawFromASingleFunder() public funded {
// AAA Pattern
// Arange
uint256 startingFundMeBalance = address(fundMe).balance;
uint256 startingOwnerBalance = fundMe.getOwner().balance;
// Act
vm.startPrank(fundMe.getOwner());
fundMe.withdraw();
vm.stopPrank();
// Assert
uint256 endingFundMeBalance = address(fundMe).balance;
uint256 endingOwnerBalance = fundMe.getOwner().balance;
assertEq(endingFundMeBalance, 0);
assertEq(
startingFundMeBalance + startingOwnerBalance,
endingOwnerBalance
);
}Hoax
Hàm sau đây kiểm tra xem owner của contract có thể rút ETH trong trường hợp có nhiều funders hay không:
function testWithdrawFromMultipleFunders() public funded {
uint160 numberOfFunders = 10;
uint160 startingFunderIndex = 1;
for (
uint160 i = startingFunderIndex;
i < numberOfFunders + startingFunderIndex;
i++
) {
// we get hoax from stdcheats
// prank + deal
hoax(address(i), SEND_VALUE);
fundMe.fund{value: SEND_VALUE}();
}
uint256 startingFundMeBalance = address(fundMe).balance;
uint256 startingOwnerBalance = fundMe.getOwner().balance;
vm.startPrank(fundMe.getOwner());
fundMe.withdraw();
vm.stopPrank();
assert(address(fundMe).balance == 0);
assert(
startingFundMeBalance + startingOwnerBalance ==
fundMe.getOwner().balance
);
assert(
(numberOfFunders + 1) * SEND_VALUE ==
fundMe.getOwner().balance - startingOwnerBalance
);
}Do kiểu address có kích thước 20 bytes - tương ứng với 160 bits nên ta sẽ dùng kiểu uint160 để vừa làm index vừa làm địa chỉ của các funders luôn.
Hàm hoax được gọi trong vòng lặp là một sự kết hợp của prank và deal. Sau đó, hàm test thực hiện fund cho contract.
Ở bước act, ta thực hiện withdraw bằng tài khoản của owner.
Ở bước assert, ta kiểm tra rằng:
- Tài khoản của contract sau khi withdraw là 0.
- Tài khoản hiện tại của owner bằng với tài khoản trước đó của owner cộng với tài khoản của contract.
- Tài khoản của owner tăng lên bằng đúng với tích của số lượng funders nhân với
SEND_VALUE. Lý do ta cộng thêm 1 là vì ta dùng modifierfunded, vốn sử dụng tài khoản củaaliceđể fund trước khi thực hiện hàm test.
Debugger
Chúng ta có thể chạy test với debugger bằng cách sử dụng flag --debug. Debugger sẽ có giao diện dòng lệnh (TUI) mà có thể tương tác như sau:

Fuzzing Testing
Để có thể gọi các hàm với nhiều đối số khác nhau, ta có thể thêm vào tham số cho hàm test.
Ví dụ, trong hàm bên dưới ta gán cứng cho đối số đầu tiên truyền vào fulfillRandomWords là 0:
function testFulfillRandomWordsCanOnlyBeCalledAfterPerformUpkeep()
public
raffleEntredAndTimePassed
{
// Arrange
// Act / Assert
vm.expectRevert((VRFCoordinatorV2_5Mock.InvalidRequest.selector);
// vm.mockCall could be used here...
VRFCoordinatorV2_5Mock(vrfCoordinator).fulfillRandomWords(
0,
address(raffle)
);
}Để test với các giá trị khác, ta sẽ thêm vào tham số cho hàm testFulfillRandomWordsCanOnlyBeCalledAfterPerformUpkeep và truyền tham số này vào hàm fulfillRandomWords:
function testFulfillRandomWordsCanOnlyBeCalledAfterPerformUpkeep(uint256 randomRequestId)
public
raffleEntredAndTimePassed
{
// Arrange
// Act / Assert
vm.expectRevert(VRFCoordinatorV2_5Mock.InvalidRequest.selector);
// vm.mockCall could be used here...
VRFCoordinatorV2_5Mock(vrfCoordinator).fulfillRandomWords(
randomRequestId,
address(raffle)
);
}Foundry sẽ tự động truyền vào tham số vừa thêm các giá trị random khi chạy test.
Số lần thực hiện fuzzing là random. Nếu ta muốn gán cứng thì có thể cấu hình như sau:
[fuzz]
runs = 1000Info
Việc chúng ta cho framework gọi các hàm trong test contract với thứ tự random và truyền vào test function các giá trị random được gọi là “open testing”. Ngoài ra, Foundry cũng sử dụng tập các random addresses khi gọi các hàm.
Chúng ta có thể chỉ định một điều kiện nào đó mà đối số random cần thỏa mãn bằng cách sử dụng vm.assume:
function testFuzz_Withdraw(uint96 amount) public {
vm.assume(amount > 0.1 ether);
// snip
}Trong hàm trên, nếu đối số mà Foundry truyền vào nhỏ hơn hoặc bằng 0.1 ether thì framework sẽ discard đối số này và chạy một run khác.
Seealso
Stateful Fuzzing Testing
Xét smart contract có một invariant (bất biến) là shouldAlwaysBeZero và ta cần test xem nó có thật sự bất biến hay không:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract CaughtWithTest {
uint256 public shouldAlwaysBeZero = 0;
uint256 private hiddenValue = 0;
function doStuff(uint256 data) public {
if (hiddenValue == 7) {
shouldAlwaysBeZero = 1;
}
hiddenValue = data;
}
}Việc fuzzing thông thường sẽ không reach được lệnh trong câu điều kiện if do state của mỗi run là độc lập với nhau. Cần ít nhất 2 lần gọi hàm doStuff để biết được rằng invariant bị vi phạm: một lần để set hiddentValue = data = 7 và một lần để set shouldAlwaysBeZero = 1. Trong trường hợp này, ta cần sử dụng stateful fuzzing testing: sử dụng một chuỗi các function calls để test function mà có duy trì state trong một run.
Trước tiên, ta cần import StdInvariant từ forge-std và cho test contract kế thừa nó.
// SPDX-License-Identifier: None
pragma solidity ^0.8.13;
import {CaughtWithTest} from "src/MyContract.sol";
import {console, Test} from "forge-std/Test.sol";
import{StdInvariant} from "forge-std/StdInvariant.sol";
contract MyContractTest is StdInvariant, Test {
CaughtWithTest myContract;
function setUp() public {
myContract = new CaughtWithTest();
}
}Sau đó truyền địa chỉ của contract cần test vào hàm targetContract:
contract MyContractTest is StdInvariant, Test {
CaughtWithTest myContract;
function setUp() public {
myContract = new CaughtWithTest();
targetContract(address(myContract));
}
}Kế đến, viết hàm test có prefix là invariant mà có chứa một assertion:
function invariant_testAlwaysReturnsZero() public view {
assert(myContract.shouldAlwaysBeZero() == 0);
}Khi chạy, nếu có một lời gọi hàm nào đó khiến cho hiddenValue trở thành 7 và theo sau đó là một lời gọi bất kỳ thì sẽ làm cho invariant bị sai.

Info
Trong thực tế, invariant có thể rất khó xác định chẳng hạn như
newTokensMinted < inflation rate, sổ xố chỉ có 1 người chiến tháng, một người dùng chỉ có thể rút những gì họ đã đặt.Ví dụ, đối với Stablecoin, invariant có thể là lượng stablecoin được minted sẽ luôn ít hơn lượng collateral được đưa vào để đảm bảo tính over-collaterized.
Khi thực hiện stateful fuzzing testing như trên, ta có thể có các cấu hình sau trong file foundry.toml:
[invariant]
runs = 128
depth = 128Với:
runs: số lần chuỗi các function calls được tạo ra và chạy.depth: số functions call trong mộtrun.
Handlers-Based Testing
Là một loại kiểm thử chuyên dụng dành cho việc kiểm thử các bất biến (invariant). Để một contract được xem là một invariant test contract, nó cần phải kế thừa StdInvariant của forge-std.
Việc sử dụng handlers-based testing giúp loại bỏ được các test case mà gây ra revert không cần thiết khi thực hiện fuzzing testing.
Invariants
Trước tiên, ta sẽ định nghĩa các invariants: định nghĩa một hoặc nhiều hàm invariant_... trong invariant test contract. Hàm này sẽ kiểm tra một điều kiện phải luôn đúng, bất kể trạng thái của hệ thống thay đổi như thế nào.
contract Invariants is StdInvariant, Test {
// ...
function invariant_protocolMustHaveMoreValueThanTotalSupply() public view {
uint256 totalSupply = dsc.totalSupply();
uint256 totalWethDeposited = IERC20(weth).balanceOf(address(dsce));
uint256 totalWbtcDeposited = IERC20(wbtc).balanceOf(address(dsce));
// Get usd value of deposited collateral
uint256 wethValue = dsce.getUsdValue(weth, totalWethDeposited);
uint256 wbtcValue = dsce.getUsdValue(wbtc, totalWbtcDeposited);
console.log("Weth Value: ", wethValue);
console.log("Wbtc Value: ", wbtcValue);
console.log("Total Supply: ", totalSupply);
assert(wethValue + wbtcValue >= totalSupply);
}
// ...
}Note
Nếu ta viết một contract kế thừa
StdInvariantnhưng không viết các invariant testing function (hàm bắt đầu bằnginvariant_) thì sẽ không test được contract đó.
Handlers
Sau đó, định nghĩa các handlers trong handler contract (hay proxy contract): các hàm này mô phỏng các hành động của người dùng, chẳng hạn như gửi tiền, rút tiền, hoặc mint token. Bản chất của các handlers là các wrapper function gọi đến các hàm của smart contract gốc. Foundry sẽ tự động gọi các handlers này với thứ tự ngẫu nhiên và các giá trị đối số ngẫu nhiên tương tự như fuzzing testing.
Constructor của handler contract sẽ là các dependencies cần thiết để phục vụ cho việc testing.
contract Handler is Test {
// ...
constructor(DSCEngine _engine, DecentralizedStableCoin _dsc) {
dsce = _engine;
dsc = _dsc;
address[] memory collateralTokens = dsce.getCollateralTokens();
weth = ERC20Mock(collateralTokens[0]);
wbtc = ERC20Mock(collateralTokens[1]);
}
// ...
}Handler sẽ bao gồm một số setup trước khi gọi hàm gốc của smart contract. Ví dụ về một handler:
function depositCollateral(
address collateral,
uint256 amountCollateral
) public {
// Setup
dsce.depositCollateral(collateral, amountCollateral);
}
Để ví dụ về các use case của setup, trong ví dụ trên, ta có thể đảm bảo rằng giá trị fuzzing truyền vào collateral chỉ có thể là các giá trị đã được định trước như sau:
function depositCollateral(
uint256 collateralSeed,
uint256 amountCollateral
) public {
ERC20Mock collateral = _getCollateralFromSeed(collateralSeed);
dsce.depositCollateral(address(collateral), amountCollateral);
}
// Helper Functions
// We use this to randomly chose the collateral for depositing
function _getCollateralFromSeed(
uint256 collateralSeed
) private view returns (ERC20Mock) {
if (collateralSeed % 2 == 0) {
return weth;
}
return wbtc;
}Ngoài ra, nếu muốn fuzzing input nằm trong một khoảng giá trị nào đó, ta có thể dùng hàm bound của Foundry. Hàm này có thể được dùng để thay thế hàm vm.assume giúp cải thiện coverage trong một số trường hợp1.
uint256 MAX_DEPOSIT_SIZE = type(uint96).max;
function depositCollateral(
uint256 collateralSeed,
uint256 amountCollateral
) public {
ERC20Mock collateral = _getCollateralFromSeed(collateralSeed);
amountCollateral = bound(amountCollateral, 1, MAX_DEPOSIT_SIZE);
dsce.depositCollateral(address(collateral), amountCollateral);
}Sau mỗi lần gọi một handler, Foundry sẽ gọi tất cả các hàm invariant_ để kiểm tra xem các tính bất biến đã được định nghĩa có còn đúng hay không. Nếu một hàm invariant_ trả về false, test sẽ thất bại và Foundry sẽ cung cấp chuỗi hành động dẫn đến lỗi đó.
targetContract
Cuối cùng, trong hàm setup của invariant test contract, ta sẽ dùng hàm targetContract của Foundry để target đến handler contract thay vì contract gốc.
contract Invariants is StdInvariant, Test {
DeployDSC deployer;
DSCEngine dsce;
DecentralizedStableCoin dsc;
HelperConfig config;
Handler handler;
address weth;
address wbtc;
function setUp() external {
deployer = new DeployDSC();
(dsc, dsce, config) = deployer.run();
(, , weth, wbtc, ) = config.activeNetworkConfig();
handler = new Handler(dsce, dsc);
targetContract(address(handler)); // target the proxy contract
}
// ...
}Tip
Nếu ta muốn tiếp tục test các invariant kể cả khi các handler function revert (chẳng hạn như khi rút tiền từ một tài khoản có số dư bằng 0) mà không khiến cho quá trình test bị dừng vì một revert nào đó thì có thể dùng option
fail_on_revert = falsetrong cấu hìnhfoundry.toml.
Ghost Variables
Chúng chỉ đơn giản là các state variable của handler contract và duy trì state giữa các function calls.
Resources
- Video: Foundry Fund Me - Writing tests for your Solidity smart contract - Foundry Fundamentals
- Video: Foundry Fund Me - Running tests on chains forks - Foundry Fundamentals
- Video: Foundry Fund Me - Foundry tests cheatcodes - Foundry Fundamentals
- Video: Foundry Fund Me - Adding more coverage to the tests - Foundry Fundamentals
- Video: Smart Contract Lottery - Intro to fuzz testing - Foundry Fundamentals
- Video: Cross Chain Rebase Token - Rebase Token Test Part 2 - Advanced Foundry
Footnotes
-
Cụ thể hơn, đối với
vm.assume, nó sẽ discard các input mà không thỏa điều kiện cho trước và điều này có thể làm ta miss các edge cases. Thay vì vậy,boundthay đổi fuzz input (thường là chia lấy phần dư) thay vì discard input. ↩