Waffle:动态模拟和测试合约调用
本教程是关于什么的?
在本教程中,你将学习如何:
- 使用动态模拟
- 测试智能合约之间的交互
本文假定:
- 你已经知道如何用
Solidity编写简单的智能合约 - 你熟悉
JavaScript和TypeScript - 你已经完成其他
Waffle教程或对其略知一二
动态模拟
为什么动态模拟很有用? 它允许我们编写单元测试,而不是集成测试。 这是什么意思呢? 这意味着我们不必担心智能合约的依赖项,因此我们可以完全隔离地测试所有合约。 让我演示一下如何才能实现。
1. 项目
在开始之前,我们需要准备一个简单的 node.js 项目:
mkdir dynamic-mockingcd dynamic-mockingmkdir contracts srcyarn init# or if you're using npmnpm init让我们从添加 typescript 和测试依赖项开始——mocha 和 chai:
yarn add --dev @types/chai @types/mocha chai mocha ts-node typescript# or if you're using npmnpm install @types/chai @types/mocha chai mocha ts-node typescript --save-dev现在让我们添加 Waffle 和 ethers:
yarn add --dev ethereum-waffle ethers# or if you're using npmnpm install ethereum-waffle ethers --save-dev你的项目结构现在应该如下所示:
1.2├── contracts3├── package.json4└── test2. 智能合约
要开始动态模拟,我们需要一个包含依赖项的智能合约。 别担心,我都准备好了!
这是一个用 Solidity 编写的简单智能合约,其唯一目的是检查我们是否有很多钱。 它使用 ERC20 代币来检查我们是否有足够的代币。 将其放在 ./contracts/AmIRichAlready.sol 中。
1pragma solidity ^0.6.2;23interface IERC20 {4 function balanceOf(address account) external view returns (uint256);5}67contract AmIRichAlready {8 IERC20 private tokenContract;9 uint public richness = 1000000 * 10 ** 18;1011 constructor (IERC20 _tokenContract) public {12 tokenContract = _tokenContract;13 }1415 function check() public view returns (bool) {16 uint balance = tokenContract.balanceOf(msg.sender);17 return balance > richness;18 }19}显示全部因为我们想使用动态模拟,所以我们不需要整个 ERC20,这就是我们使用只有一个函数的 IERC20 接口的原因。
现在是构建这个合约的时候了! 为此,我们将使用 Waffle。 首先,我们将创建一个简单的 waffle.json 配置文件,它指定了编译选项。
1{2 "compilerType": "solcjs",3 "compilerVersion": "0.6.2",4 "sourceDirectory": "./contracts",5 "outputDirectory": "./build"6}现在我们已经准备好使用 Waffle 构建合约:
npx waffle很简单,对吗? 在 build/ 文件夹中,出现了与合约和接口相对应的两个文件。 我们稍后将使用它们进行测试。
3. 测试
让我们创建一个名为 AmIRichAlready.test.ts 的文件来进行实际测试。 首先,我们必须处理导入。 我们稍后将需要它们:
1import { expect, use } from "chai"2import { Contract, utils, Wallet } from "ethers"3import {4 deployContract,5 deployMockContract,6 MockProvider,7 solidity,8} from "ethereum-waffle"除了 JS 依赖项,我们还需要导入已构建的合约和接口:
1import IERC20 from "../build/IERC20.json"2import AmIRichAlready from "../build/AmIRichAlready.json"Waffle 使用 chai 进行测试。 但是,在使用它之前,我们必须将 Waffle 的匹配器注入到 chai 本身:
1use(solidity)我们需要实现 beforeEach() 函数,它将在每次测试前重置合约的状态。 让我们先想想我们需要什么。 要部署一个合约,我们需要两样东西:一个钱包和一个已部署的 ERC20 合约,以便将其作为 AmIRichAlready 合约的参数传递。
首先,我们创建一个钱包:
1const [wallet] = new MockProvider().getWallets()然后我们需要部署一个 ERC20 合约。 这里是棘手的部分——我们只有一个接口。 这正是 Waffle 大显身手的时候。 Waffle 有一个神奇的 deployMockContract() 函数,它只使用接口的 abi 就能创建一个合约:
1const mockERC20 = await deployMockContract(wallet, IERC20.abi)现在有了钱包和已部署的 ERC20,我们就可以继续部署 AmIRichAlready 合约了:
1const contract = await deployContract(wallet, AmIRichAlready, [2 mockERC20.address,3])做完这些,我们的 beforeEach() 函数就完成了。 到目前为止,你的 AmIRichAlready.test.ts 文件应该如下所示:
1import { expect, use } from "chai"2import { Contract, utils, Wallet } from "ethers"3import {4 deployContract,5 deployMockContract,6 MockProvider,7 solidity,8} from "ethereum-waffle"910import IERC20 from "../build/IERC20.json"11import AmIRichAlready from "../build/AmIRichAlready.json"1213use(solidity)1415describe("Am I Rich Already", () => {16 let mockERC20: Contract17 let contract: Contract18 let wallet: Wallet1920 beforeEach(async () => {21 ;[wallet] = new MockProvider().getWallets()22 mockERC20 = await deployMockContract(wallet, IERC20.abi)23 contract = await deployContract(wallet, AmIRichAlready, [mockERC20.address])24 })25})显示全部让我们为 AmIRichAlready 合约编写第一个测试。 你认为我们的测试应该是关于什么的? 没错,你是对的! 我们应该检查我们是否有很多钱:)
但是等一下。 我们的模拟合约怎么知道要返回什么值呢? 我们还没有为 balanceOf() 函数实现任何逻辑。 同样,Waffle 在这里也可以提供帮助。 我们的模拟合约现在有了一些新花招:
1await mockERC20.mock.<nameOfMethod>.returns(<value>)2await mockERC20.mock.<nameOfMethod>.withArgs(<arguments>).returns(<value>)有了这些知识,我们终于可以编写第一个测试了:
1it("如果钱包中的代币少于 1000000,则返回 false", async () => {2 await mockERC20.mock.balanceOf.returns(utils.parseEther("999999"))3 expect(await contract.check()).to.be.equal(false)4})我们来把这个测试分解成几个部分:
- 我们将我们的模拟 ERC20 合约设置为始终返回 999999 个代币的余额。
- 检查
contract.check()方法是否返回false。
我们已经准备好启动这个家伙了:
所以测试通过了,但是…… 还有一些改进的空间。 balanceOf() 函数将始终返回 99999。 我们可以通过指定一个钱包来改进它,让函数为该钱包返回一些东西——就像一个真正的合约一样:
1it("如果钱包中的代币少于 1000001,则返回 false", async () => {2 await mockERC20.mock.balanceOf3 .withArgs(wallet.address)4 .returns(utils.parseEther("999999"))5 expect(await contract.check()).to.be.equal(false)6})到目前为止,我们只测试了我们不够有钱的情况。 让我们来测试一下相反的情况:
1it("如果钱包中至少有 1000001 个代币,则返回 true", async () => {2 await mockERC20.mock.balanceOf3 .withArgs(wallet.address)4 .returns(utils.parseEther("1000001"))5 expect(await contract.check()).to.be.equal(true)6})你运行测试……
……就这样,大功告成! 我们的合约似乎按计划进行 :)
测试合约调用
让我们总结一下到目前为止所做的事情。 我们已经测试了我们的 AmIRichAlready 合约的功能,它似乎正常工作。 这意味着我们已经完成了,对吧? 并非如此! Waffle 允许我们进一步测试合约。 但是具体怎么做呢? 那么,在 Waffle 的武器库中,有一个 calledOnContract() 和 calledOnContractWith() 匹配器。 它们允许我们检查,我们的合约是否调用了 ERC20 模拟合约。 下面是使用其中一个匹配器的基本测试:
1it("检查合约是否在 ERC20 代币上调用了 balanceOf", async () => {2 await mockERC20.mock.balanceOf.returns(utils.parseEther("999999"))3 await contract.check()4 expect("balanceOf").to.be.calledOnContract(mockERC20)5})我们可以更进一步,用我告诉你的另一个匹配器改进这个测试:
1it("检查合约是否在 ERC20 代币上用特定钱包调用了 balanceOf", async () => {2 await mockERC20.mock.balanceOf3 .withArgs(wallet.address)4 .returns(utils.parseEther("999999"))5 await contract.check()6 expect("balanceOf").to.be.calledOnContractWith(mockERC20, [wallet.address])7})让我们检查测试是否正确:
太好了,所有测试都通过了。
用 Waffle 测试合约调用非常容易。 而这是最精彩的部分。 这些匹配器对正常合约和模拟合约都有效! 这是因为 Waffle 记录并过滤 EVM 调用,而不是像其他技术的流行测试库那样注入代码。
终点线
恭喜! 现在你知道如何使用 Waffle 来测试合约调用和动态模拟智能合约了。 还有更多有趣的功能等着你去发现。 我建议你深入研究 Waffle 文档。
Waffle 的文档可以在此处opens in a new tab查看。
本教程的源代码可以在此处opens in a new tab找到。
你可能还感兴趣的教程:
页面最后更新: 2024年2月27日


