Waffle:动态模拟和测试合约调用
本教程是关于什么的?
在本教程中,你将学习如何:
- 使用动态模拟
- 测试智能合约之间的交互
本文假定:
- 你已经知道如何在
Solidity
中编写一个简单的智能合约 - 你熟悉
JavaScript
和TypeScript
- 你已经完成其他
Waffle
教程或对其略知一二。
动态模拟
为什么动态模拟有用? 它允许我们编写单元测试,而不是集成测试。 这是什么意思呢? 这意味着我们不必担心智能合约的依赖性,因此我们可以完全孤立地测试所有合约。 让我演示一下如何才能实现。
1. 项目
在开始之前,我们需要准备一个简单的node.js项目:
mkdir dynamic-mockingcd dynamic-mockingmkdir contracts srcyarn init# or if you're using npmnpm init
让我们从添加类型脚本和测试依赖项开始-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└── test
2. 智能合约
要开始动态模拟,我们需要一个包含依赖项的智能合约。 别担心,我会掩护你的!
这是一个用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}复制
现在我们已经准备好使用Waffer构建合约:
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("returns false if the wallet has less than 1000000 tokens", async () => {2 await mockERC20.mock.balanceOf.returns(utils.parseEther("999999"))3 expect(await contract.check()).to.be.equal(false)4})
让我们把这个测试分解成几个部分:
- 将我们的模拟ERC20合约设置为始终返回99999个通证的余额。
- 检查
contract.check()
方法是否返回false
。
我们已经准备好启动这个野兽了:
所以这个测试是有效的,但是......还是有一些改进的余地。 balanceOf()
函数将始终返回99999。 我们可以通过指定一个钱包来改进它,该函数应该为它返回一些东西 -- 就像一个真正的合约。
1it("returns false if the wallet has less than 1000001 tokens", 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("returns true if the wallet has at least 1000001 tokens", 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("checks if contract called balanceOf on the ERC20 token", async () => {2 await mockERC20.mock.balanceOf.returns(utils.parseEther("999999"))3 await contract.check()4 expect("balanceOf").to.be.calledOnContract(mockERC20)5})
我们可以更进一步,用我告诉你的另一个匹配器改进这个测试:
1it("checks if contract called balanceOf with certain wallet on the ERC20 token", 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)找到。
你可能还感兴趣的教程:
上次修改时间: @nhsz(opens in a new tab), 2024年2月27日