FundMe 合约编写及高覆盖率测试
本文目标是完善 FundMe.sol
合约,并为该合约进行高覆盖率测试。
FundMe 合约编写
首先,我们需要写出 FundMe.sol
合约:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
import {PriceConverter} from "./PriceConverter.sol";
error NotOwner();
contract FundMe {
using PriceConverter for uint256;
mapping(address => uint256) private s_addressToAmountFunded;
address[] private s_funders;
address private immutable i_owner;
uint256 public constant MINIMUM_USD = 5e18;
AggregatorV3Interface private s_priceFeed;
modifier onlyOwner {
if (msg.sender != i_owner) revert NotOwner();
_;
}
constructor(address priceFeed) {
i_owner = msg.sender;
s_priceFeed = AggregatorV3Interface(priceFeed);
}
function fund() public payable {
require(msg.value.getConversionRate(s_priceFeed)) >= MINIMUM_USD, "You need to spend more ETH!";
s_addressToAmountFunded[msg.sender] += msg.value;
s_funder.push(msg.sender);
}
function withdraw() public onlyOwner {
for (
uint256 i = 0;
i < s_funders.length;
i++
) {
address funder = s_funders[i];
s_addressToAmountFunded[funder] = 0;
}
s_funders = new address[](0);
(bool callSuccess, ) = payable(msg.sender).call{ value: address(this).balance } ("");
require(callSuccess, "Call failed");
}
/* Getter Functions */
function getAddressToAmountFunded(address fundingAddress) external view returns(uint256) {
return s_addressToAmountFunded[fundingAddress];
}
function getFunder(uint256 index) public view returns(address) {
return s_funders[index];
}
function getOwner() public view returns(address) {
return i_owner;
}
function getPriceFeed() public view returns(AggregatorV3Interface) {
return s_priceFeed;
}
function getVersion() public view returns (uint256) {
return s_priceFeed.version();
}
}
该 FundMe 合约可以大致分为以下五个部分:
- 导入和声明
这部分包括合约所需要的库的导入和合约的版本声明。在 FundMe 合约中,这包括pragma solidity ^0.8.19
,以及从其他文件中导的接口和库。 - 状态变量和类型定义
这部分包含合约的状态变量和类型定义。在 FundMe 合约中,这包括:s_addressToAmountFunded
,s_funders
,i_owner
,MINIMUM_USD
, 以及s_priceFeed
这五个存储变量的定义。 - 构造函数和修饰符
这部分包括构造函数和修饰符的定义。在 FundMe 合约中,这包括constructor(address priceFeed)
和modifier onlyOwner
。 - 主要功能函数
这部分包括合约的主要功能函数。在 FundMe 合约中,这包括fund()
和withdraw()
。 - Getter 函数
这部分包括所有的 getter 函数,这些函数用来读取合约的状态变量。在 FundMe 中,这包括getAddressToAmountFunded
,getFunder
,getOwner
,getPriceFeed
, 和getVersion
函数。
FundMe 合约测试
在测试的过程中,我们要将测试重心将放在「状态变量和类型定义」以及「主要功能函数」这两个部分上。
测试合约 FundMe.t.sol
代码如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
/* 导入和声明 */
import "forge-std/Test.sol";
import "../src/FundMe.sol";
import "../script/FundMe.s.sol";
contract FundMeTest is Test {
/* 状态变量和类型定义 */
FundMe fundMe;
address immutable USER = makeAddr("Corror");
uint256 constant TRANSFER_AMOUNT = 10 ether;
uint256 constant ZERO_BALANCE = 0 ether;
/* 环境设置函数和修饰符 */
function setUp() public {
FundMeScript fundMeScript = new FundMeScript();
fundMe = fundMeScript.run();
}
modifier funded() {
hoax(USER);
fundMe.fund{value: TRANSFER_AMOUNT};
_;
}
/* 状态变量测试 */
function testMINIMUM_USD() public {
assertEq(fundMe.MINIMUM_USD(), 5e18);
}
function testI_owner() public {
assertEq(fundMe.getOwner(), msg.sender);
}
function testGetVersion() public {
assertEq(fundMe.getVersion(), 4);
}
/* 主要功能函数测试 */
/* 1. fundMe() 函数测试 */
function testFundRevertWithBelowMinimumTransactionAmount() public {
hoax(USER);
vm.expectRevert();
fundMe.fund{value: 0}();
}
function testFundUpdatesAmountFundedOnSuccess() funded public {
assertEq(fundMe.getAddressToAmountFunded(USER), TRANSFER_AMOUNT);
}
function testFundAddFunderOnSuccess() funded public {
assertEq(fundMe.getFunder(0), USER);
}
/* 2. 测试 withdraw() 函数 */
function testWithdrawRevertWhenCallerIsNotOwner() funded public {
vm.prank(USER);
vm.expectRevert();
fundMe.withdraw();
}
function testWithdrawSingerFund() funded public {
hoax(fundMe.getOwner(), ZERO_BALANCE);
fundMe.withdraw();
assertEq(fundMe.getOwner().balance, TRANSFER_AMOUNT);
}
function testWithrawMultipulFunds() public {
// 准备:Arrange
uint160 fundersAmount = 10;
uint160 initialFunderIndex = 1;
for (
uint160 fundersIndex=initialFunderIndex;
funderIndex<=fundersAmount;
fundersIndex++
) {
hoax(address(fundersIndex));
fundMe.fund(value: TRANSFER_AMOUNT)();
}
// 执行:Act
hoax(fundMe.getOwner(), 0);
fundMe.withdraw();
// 断言:Assert
assertEq(fundMe.getOwner().balance, fundersAmount * TRANSFER_AMOUNT);
}
}
覆盖率检测
写出测试合约后,我们可以通过 Foundry 命令检查测试覆盖率:
forge coverage
在代码正确的情况下,覆盖率应该为 93.75%。