Q1ngying

今朝梦醒与君别,遥盼春风寄相思

0%

Foundry入门

Foundry

开始

使用 Foundry 框架来开发或测试智能合约时,第一步需要在空白文件夹中使用命令:

1
forge init

然后,Foundry 会在项目目录下生成一些文件,这些文件有一些是用于测试合约的 Solidity 脚本库。

注意,在 Foundry 框架中,所有的“真正的”合约要放在 *src目录中,脚本合约要放在script目录中,测试合约要放在test*目录中。同时,Foundry 还规定,脚本合约的后缀名为.s.sol,测试合约的后缀名为.t.sol

使用 Foundry 进行编译

在 Foundry 框架中:

1
forge compile(或者 forge build)

使用该命令后,会在项目目录里生成两个新的目录:*out·cache*

在本地区块链上进行合约测试

  • Foundry 内置 anvil
  • Ganache

使用 Foundry 对合约进行部署

在命令行

1
forge create "合约名称" --rpc-url "URL" --interactive (私钥,测试时可以,注意堤防私钥泄露)

部署合约后,会在工作目录中生成两个新的目录

使用 solidity 脚本部署

使用 Solidity 脚本部署合约,需要在*script*目录中新建一个.sol文件,以下面的例子举例:

需要部署的文件为SimpleStorage.sol(位于 *src*目录下)合约代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.19;

// pragma solidity ^0.8.0;
// pragma solidity >=0.8.0 <0.9.0;

contract SimpleStorage {
uint256 myFavoriteNumber;

struct Person {
uint256 favoriteNumber;
string name;
}
// uint256[] public anArray;
Person[] public listOfPeople;

mapping(string => uint256) public nameToFavoriteNumber;

function store(uint256 _favoriteNumber) public {
myFavoriteNumber = _favoriteNumber;
}

function retrieve() public view returns (uint256) {
return myFavoriteNumber;
}

function addPerson(string memory _name, uint256 _favoriteNumber) public {
listOfPeople.push(Person(_favoriteNumber, _name));
nameToFavoriteNumber[_name] = _favoriteNumber;
}
}

Solidity 脚本书写

而要使用 Solidity 脚本部署这个合约,可以在*script*目录下新建一个合约(我将其命名为DeploySimpleStorage.s.sol注意,后缀名最好为.s.sol,因为这是一个脚本文件。DeploySimpleStorage.s.sol代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.18;

import {Script} from "forge-std/Script.sol";
import {SimpleStorage} from "../src/SimpleStorage.sol";

contract DeploySimpleStorage is Script {
function run() external returns(SimpleStorage) {
vm.startBroadcast();
// 要部署的合约代码
SimpleStorage simplestprage = new SimpleStorage();
vm.stopBroadcast();
return (simplestprage);
}
}

上述脚本代码中,vm.startBroadcast()vm.stopBroadcast()是一块出现的,中间的部分即为要部署的合约,可以 new 关键字新建合约,同时,记得使用 return 关键字返回部署的合约的地址。

Solidity 脚本运行

要运行上述脚本,需要在 bash 中使用下面的指令:

1
forge script script/DeploySimpleStorage.s.sol

这样会将合约部署在一个临时的 anvil 链上。如果想要模拟真正的交易,需要传递一个 rpc-url:

1
forge script script/DeploySimpleStorage.s.sol --rpc-url "rpc" 

上面的指令会模拟部署到 anvil 链上,同时还创建了一个新的目录,他会存放我们之前部署的信息

如果想要广播这个消息,需要加上--broadcast

1
forge script script/DeploySimpleStorage.s.sol --rpc-url "rpc" --broadcast --private-key "key"

上述的命令中DeploySimpleStorage.s.sol可以替换为其他部署合约的脚本。

使用上述命令部署合约后同样在目录 *broadcast下,存储部署这个合约时的信息。不同的是,没使用--broadcast选项的信息会存储在broadcast的子文件夹dry-run*下。

使用 thiedweb 部署合约

1
npx thirdweb deploy

cast命令

cast --to-base将十六进制转换为十进制

1
cast --to-base 0x714c2 dec

返回结果:464066

cast send 发送签名和交易(调用非只读函数)

Foundry 内置工具 cast 中有一个可以使用的命令**send**用于签名和发布交易:

1
cast send "合约地址" "函数签名" "参数值" --rpc-url "rpc" --private-key "privatekey"

比如:

1
cast send 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 "store(uint256)" 123 --rpc-url $rpc --private-key $key0

cast call 从区块中读取数据(调用只读函数)

call 命令类似于 Remix 中的蓝色按钮,只能调用函数,不能发送交易

1
cast call "合约地址" "函数名/变量名" --rpc-url "rpc"
1
cast call 0xf0a005d0697a6518a09A378f371f0058a69F5c3B "retrieve()" --rpc-url $rpc

加快与合约的交互方法(仅在开发调试时使用,实际生产中不可取)

在文件根目录新建一个.env文件。该文件格式可类似为:

1
2
3
key0=0x517e11ba636227ffbdb3a4c8e2f2db11c9cc7fef4c290f6c88f28f76f5c9f789
rpc=http://127.0.0.1:7545
key1=0xc4c1bae53fb65f8549a2034f8dcc92f5be0840981912db5701435def7167099a

创建这个文件后,打开.gitignore文件,确保.env文件在其中被忽略。

.env中,我们可以设置“环境变量”。设置后,在终端运行:

1
source .env

在测试网上模拟在主网中的操作

部署合约到测试网

要将合约部署到测试网上,需要有测试网的 rpc ,这里使用 Alchemy 来构建一个在 Sepolia 测试网上的项目,便会得到一个我们自己的在 Sepolia 测试网上的 rpc,同时,我们部署了合约后,可以在 Alchemy 面板上查看我们部署的合约。

而部署的过程和刚刚在本地部署的过程没有很大的区别,唯一的区别就是需要把参数中的rpcprivate-key换为我们在 Alchemy 生成的 rpc ,自己Meta Mask钱包中拥有 Sepolia ETH 的私钥。注意,如果要把自己的 Meta Mask 钱包的私钥放进 .env 环境变量之前,确保这个私钥在 ETH 主网上没有余额!!!

合约验证

手动验证

在 Etherscan 或者其他区块链浏览器上进行验证。

image-20231014153755212

点击上方的合约验证

image-20231014153831492

按照上述要求进行验证

image-20231014153952813

继续操作,即可验证成功。

验证成功后,可以在 Etherscan 上看到我们的合约源码,并且可以在区块链浏览器上与合约进行交互

清理项目

当我们的项目基本上完成后,我们就可以对我们的项目进行最后一步的加工

格式化 solidity 代码

可以通过在终端使用forge fmt命令对 solidity 代码进行格式化。**这是一个可以格式化我们所有的 solidity 代码的命令。

创建 README.md

创建 README.md 文件对我们的项目进行描述,可以包含:

项目信息、使用说明、联系方式等。

Foundry 调试

在 Foundry 中进行测试时,如果不指定链,Foundry 会自动启动一个 anvil 链。

解决在 Remix 导入合约报错的问题

在 Remix 编译器中,可以通过使用"@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"这种方式导入合约,但是使用其他编译器时编译器会报错,原因在于:在 Remix 收纳柜使用@chainlink/contracts时,Remix 会自动在 npm 包存储库中查找,而 Foundry 需要明确地知道从哪里获取依赖项。所以,我们要直接从 Github 下载,我们使用forge install命令安装这个依赖项:

1
forge install smartcontractkit/chainlink-brownie-contracts

在大多数情况下,还需要添加--no-commit命令,同时还可以使用@来选择版本,所以最终的代码为:

1
forge install smartcontractkit/chainlink-brownie-contracts@0.6.1 --no-commit

接下来要在 foundry.toml文件中创建一个叫做remappings的重映射:

profile.dedfault下面创建一个叫做remappings的新部分:

1
2
3
4
5
6
7
8
9
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
remappings = ["@chainlink/contracts/=lib/chainlink-brownie-contracts/contracts"]


# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options

最好的做法是,在命名错误时,使用合约名称加上两个下划线,这样当遇到这个错误时,可以很容易地知道是哪个合约出了问题

forge test 参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
forge test # 进行所有的测试
forge test -vv # 改变可见性,v越多可见性越高
# 如果我们在测试的时候没有为其提供 RPC URL 时,他会启动一个全新的空白 anvil 链进行测试
fork test --fork-url "url"
# 上述命令中,anvil 被启动,但他会使用 Sepolia RPC URL 的副本来运行,他会启动一个 anvil 链,并模拟所有的交易,就像它们在 Sepolia 链上运行一样,他会假装部署和读取 Sepolia 链上的内容,而不是一个完全空白的链
--fail-fast 快速失败,第一次失败后停止测试
--gas-report 打印 gas 报告
--match-test <正则表达式>,仅运行与指定正则表达式匹配的测试函数,(别名-mt)
--no-match-test <正则表达式>,仅运行与指定正则表达式不匹配的合约测试,(别名-nmt)
--match-contract <正则表达式>,仅运行与指定正则表达式匹配的合约测试,(别名-mc)
--NO-match-contract <正则表达式>,仅运行与指定正则表达式不匹配的合约测试,(别名-nmc)
-f
--fork-url <URL> 通过远程端点获取状态而不是从一个空的状态
--fork-block-number <块> 通过远程从特定块号获取状态端点。
-v EVM 的详细程度。
多次传递以增加详细程度(例如 -v、-vv、-vvv)。
详细程度:
- 2:打印所有测试的日志
- 3:打印失败测试的执行跟踪
- 4:打印所有测试的执行跟踪以及设置跟踪对于失败的测试
- 5:打印所有测试的执行和设置跟踪
forge coverage : 可以查看实际测试套件中实际测试了多少代码

基本测试

在 Foundry 框架中,有一些内置库和工具可以十分方便的测试合约,所以在编写一个测试合约的时候大致框架如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pragma solidity ^0.8.0;

import {Test} from "forge-std/Test.sol"

contract Name is Test {
function setUp() external {}
}
// 我们可以进行以下四种测试:
// 1. 单元测试
// - 测试代码中的一个特定部分
// 2. 集成测试
// - 测试多个不同合约是否能够正确地协同工作
// 3. Fork测试(可以认为时单元/集成测试的一部分)
// - 模拟在真实环境中对代码进行测试
// 4. Staging 测试
// - 将代码部署到测试网或者主网之类的环境中,并在这个真实环境中运行所有测试以确保事情能够正常工作

简单的测试

在所有的测试中,第一件都是设置 setUp() 函数,在这里部署合约,然后就可以写需要进行的测试,举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pragma solidity ^0.8.0;

import {Test} from "forge-std/Test.sol"

contract Name is Test {
uint number = 1;

function setUp() external {
number = 2;
}

function testDemo() public {
assertEq(number, 2);
}
}

测试结果:

image-20231016145433438

console.log 测试、调试

另一种测试和调试的方法是使用console.log,Test是一个很大的库,还有另一个库叫做 console

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import {Test, console} from "forge-std/Test.sol";

contract FundMeTest is Test {

uint number = 1;

function setUp() external {
number = 2;
}

function testDemo() external {
console.log(number);
console.log("Hello!");
assertEq(number, 2);
}

}

测试结果:

image-20231016145651779

对 FundMe 合约进行简单的调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import {Test, console} from "forge-std/Test.sol";
import {FundMe} from "../src/FundMe.sol";

contract FundMeTest is Test {
FundMe fundMe; // 定义 FundMe 合约变量

function setUp() external {
fundMe = new FundMe(); // 实例化一个 FundMe 合约 fundme
}

function testMininumDollarIsFive() public {
assertEq(fundMe.MINIMUM_USD(), 5e18);
// 对 FundMe 合约进行调试
}
}

检验合约所有者是否为 msg.sender

可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import {Test, console} from "forge-std/Test.sol";
import {FundMe} from "../src/FundMe.sol";

contract FundMeTest is Test {
FundMe fundMe; // 定义 FundMe 合约变量

function setUp() external {
fundMe = new FundMe(); // 实例化一个 FundMe 合约 fundme
}

function testMininumDollarIsFive() public {
assertEq(fundMe.MINIMUM_USD(), 5e18);
// 对 FundMe 合约进行调试
}

function testOwnerIsMsgSender() public {
assertEq(fundMe.i_owner(), msg.sender);
}
}

经过 forge 的调试,我们发现msg.sender不是合约的所有者,我们可以对上面的程序进行进一步的调整:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import {Test, console} from "forge-std/Test.sol";
import {FundMe} from "../src/FundMe.sol";

contract FundMeTest is Test {
FundMe fundMe; // 定义 FundMe 合约变量

function setUp() external {
fundMe = new FundMe(); // 实例化一个 FundMe 合约 fundme
}

function testMininumDollarIsFive() public {
assertEq(fundMe.MINIMUM_USD(), 5e18);
// 对 FundMe 合约进行调试
}

function testOwnerIsMsgSender() public {
console.log(fundMe.i_owner());
console.log(msg.sender);
assertEq(fundMe.i_owner(), msg.sender);
}
}

image-20231016151352815

经过测试发现:我 -> FundMeTest -> FundMe,我们不应该检验我们和是否为所有者,而是检测 FundMeTest 是不是所有者,所以将上面的testOwnerIsMsgSender函数修改为下面的形式即可:

1
2
3
4
5
function testOwnerIsMsgSender() public {
console.log(fundMe.i_owner());
console.log(msg.sender);
assertEq(fundMe.i_owner(), address(this));
}

检验版本(使用–fork-url 模拟实际链上的内容)

1
2
3
4
function testPriceFeedVersionIsAccurate() public { // 
uint256 version = fundMe.getVersion();
assertEq(version, 4);
}

对这个函数进行测试时会报错,原因在于,FundMe 合约中的参数地址是 Sepolia 链上的地址,而我们在 Foundry 上测试其实是部署在一个新建的 anvil 链上,并在经过测试和自动关闭 anvil 链的。所以,这里就需要用到了 --fork-url选项,详见[forge test参数](##forge test 参数)

然后,执行下面的bash命令会发现程序成功进行:

1
forge test --match-test  testPriceFeedVersionIsAccurate --fork-url https://eth-sepolia.g.alchemy.com/v2/htfp6AGYmR20uiC5S-rZfe7IZvuxManb

当我们使用 Frok-URL 时,我们会模拟实际链上的内容,这是一个在实际网络上轻松测试合约的一个好方法。但是,这些 Fork 的缺点是会对 Alchemy 节点进行大量的 API 调用,这可能会产生更多的费用,所以尽可能多的进行测试而不使用 Fork 是很重要的,当然除了一些必须在 Fork 上运行的测试。或者使用模拟来确保我们有足够的覆盖率来测试所有的合约。

测试一个是否发生回滚

有些时候我们需要交易回滚,所以我们可以通过Foundry来书写测试脚本来检查这个合约是否回滚:

1
2
3
4
5
function testFundFailsWithoutEnoughETH() public{
vm.expectRevert(); // 这个函数的下一行应该回滚
// 等价于: assert(This tx fails/reverts)
uint256 cat = 1;
}

在 bash 中 对上面这个函数进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ forge test --mt testFundFailsWithoutEnoughETH
[⠢] Compiling...
[⠃] Compiling 1 files with 0.8.21
[⠊] Solc 0.8.21 finished in 670.25ms
Compiler run successful with warnings:
Warning (2072): Unused local variable.
--> test/FundeMeTest.t.sol:44:9:
|
44 | uint256 cat = 1;
| ^^^^^^^^^^^


Running 1 test for test/FundeMeTest.t.sol:FundMeTest
[FAIL. Reason: Call did not revert as expected] testFundFailsWithoutEnoughETH() (gas: 3047)
Test result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 1.32ms

Ran 1 test suites: 0 tests passed, 1 failed, 0 skipped (1 total tests)

Failing tests:
Encountered 1 failing test in test/FundeMeTest.t.sol:FundMeTest
[FAIL. Reason: Call did not revert as expected] testFundFailsWithoutEnoughETH() (gas: 3047)

Encountered a total of 1 failing tests, 0 tests succeeded

如上所示,上面的测试失败,因为vm.expectRevert()的下一行语句没有发生回滚。

1
2
3
4
5
function testFundFailsWithoutEnoughETH() public{
vm.expectRevert(); // the next line, should revert!
// 等价于: assert(This tx fails/reverts)
fundMe.fund(); // send 0 value,根据fundme合约中的fund函数,会发生回滚。
}

结果如下:

1
2
3
4
5
6
7
8
9
10
11
$ forge test --mt testFundFailsWithoutEnoughETH
[⠢] Compiling...
[⠊] Compiling 1 files with 0.8.21
[⠒] Solc 0.8.21 finished in 727.78ms
Compiler run successful!

Running 1 test for test/FundeMeTest.t.sol:FundMeTest
[PASS] testFundFailsWithoutEnoughETH() (gas: 13305)
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.92ms

Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)

交易发生回滚,是我们预期的结果。所以测试通过。

Alchemy

Alchemy 简述

Alchemy 是一个提供非托管的 Web3 开发者工具和平台,为 Web3 和 Web2 应用程序提供服务。

Alchemy 可以理解为是一个 Web3 的云服务器(类似于 华为云,阿里云),它提供了 API SDK 和库,可以让开发者有更好的体验。

它通过三点实现:

  • 超级节点:
    他是一个基于我的节点的负载均衡器,可以确保始终从区块链中获取到最新的可用数据。在超级节点上,Alchemy 构建了公告 API (Announced APIs:公告 API 是一组 API,可以方便地从区块链中提取数据)

开始构建

登录 Alchemy 官网,选择登录,进入页面就可以看到自己账号的所有项目(前提是已经创建完了)。 点击创建应用程序即可创建新的项目

image-20231014161215982

创建成功后,点击我们创建的项目即可进入应用程序特定的仪表盘

Alchemy 是一个在实际开发 Web3 应用时很实用的一个工具,其余内容可查看 Alchemy 官方文档

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
// FundMe.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
import {PriceConverter} from "./PriceConverter.sol";

error FundMe_NotOwner();

contract FundMe {
using PriceConverter for uint256;

mapping(address => uint256) public addressToAmountFunded;
address[] public funders;

// Could we make this constant? /* hint: no! We should make it immutable! */
address public /* immutable */ i_owner;
uint256 public constant MINIMUM_USD = 5 * 10 ** 18;

constructor() {
i_owner = msg.sender;
}

function fund() public payable {
require(msg.value.getConversionRate() >= MINIMUM_USD, "You need to spend more ETH!");
// require(PriceConverter.getConversionRate(msg.value) >= MINIMUM_USD, "You need to spend more ETH!");
addressToAmountFunded[msg.sender] += msg.value;
funders.push(msg.sender);
}

function getVersion() public view returns (uint256){
AggregatorV3Interface priceFeed = AggregatorV3Interface(0x694AA1769357215DE4FAC081bf1f309aDC325306);
return priceFeed.version();
}

modifier onlyOwner {
// require(msg.sender == owner);
if (msg.sender != i_owner) revert FundMe_NotOwner();
_;
}

function withdraw() public onlyOwner {
for (uint256 funderIndex=0; funderIndex < funders.length; funderIndex++){
address funder = funders[funderIndex];
addressToAmountFunded[funder] = 0;
}
funders = new address[](0);
// // transfer
// payable(msg.sender).transfer(address(this).balance);

// // send
// bool sendSuccess = payable(msg.sender).send(address(this).balance);
// require(sendSuccess, "Send failed");

// call
(bool callSuccess, ) = payable(msg.sender).call{value: address(this).balance}("");
require(callSuccess, "Call failed");
}
// Explainer from: https://solidity-by-example.org/fallback/
// Ether is sent to contract
// is msg.data empty?
// / \
// yes no
// / \
// receive()? fallback()
// / \
// yes no
// / \
//receive() fallback()

fallback() external payable {
fund();
}

receive() external payable {
fund();
}

}

// Concepts we didn't cover yet (will cover in later sections)
// 1. Enum
// 2. Events
// 3. Try / Catch
// 4. Function Selector
// 5. abi.encode / decode
// 6. Hash with keccak256
// 7. Yul / Assembly
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// PriceConverter.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

// Why is this a library and not abstract?
// Why not an interface?
library PriceConverter {
// We could make this public, but then we'd have to deploy it
function getPrice() internal view returns (uint256) {
// Sepolia ETH / USD Address
// https://docs.chain.link/data-feeds/price-feeds/addresses
AggregatorV3Interface priceFeed = AggregatorV3Interface(
0x694AA1769357215DE4FAC081bf1f309aDC325306
);
(, int256 answer, , , ) = priceFeed.latestRoundData();
// ETH/USD rate in 18 digit
return uint256(answer * 10000000000);
}

// 1000000000
function getConversionRate(
uint256 ethAmount
) internal view returns (uint256) {
uint256 ethPrice = getPrice();
uint256 ethAmountInUsd = (ethPrice * ethAmount) / 1000000000000000000;
// the actual ETH/USD conversion rate, after adjusting the extra 0s.
return ethAmountInUsd;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// FundMeTest.t.sol
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import {Test, console} from "forge-std/Test.sol";
import {FundMe} from "../src/FundMe.sol";

contract FundMeTest is Test {
FundMe fundMe;

function setUp() external {
fundMe = new FundMe(0x694AA1769357215DE4FAC081bf1f309aDC325306);
}

function testMininumDollarIsFive() public {
assertEq(fundMe.MINIMUM_USD(), 5e18);
}

function testOwnerIsMsgSender() public {
console.log(fundMe.i_owner());
console.log(msg.sender);
assertEq(fundMe.i_owner(), address(this));
}
// 我们可以进行以下四种测试:
// 1. 单元测试
// - 测试代码中的一个特定部分
// 2. 集成测试
// - 测试多个不同合约是否能够正确地协同工作
// 3. Fork测试(可以认为时单元/集成测试的一部分)
// - 模拟在真实环境中对代码进行测试
// 4. Staging 测试
// - 将代码部署到测试网或者主网之类的环境中,并在这个真实环境中运行所有测试以确保事情能够正常工作
function testPriceFeedVersionIsAccurate() public { //
uint256 version = fundMe.getVersion();
assertEq(version, 4);
}
}