跳转至主要内容

Waffle:动态模拟和测试合约调用

waffle
智能合同
Solidity
测试
模拟
中级
Daniel Izdebski
2020年11月14日
10 分钟阅读

本教程是关于什么的?

在本教程中,你将学习如何:

  • 使用动态模拟
  • 测试智能合约之间的交互

本文假定:

  • 你已经知道如何用 Solidity 编写简单的智能合约
  • 你熟悉 JavaScriptTypeScript
  • 你已经完成其他 Waffle 教程或对其略知一二

动态模拟

为什么动态模拟很有用? 它允许我们编写单元测试,而不是集成测试。 这是什么意思呢? 这意味着我们不必担心智能合约的依赖项,因此我们可以完全隔离地测试所有合约。 让我演示一下如何才能实现。

1. 项目

在开始之前,我们需要准备一个简单的 node.js 项目:

mkdir dynamic-mocking
cd dynamic-mocking
mkdir contracts src
yarn init
# or if you're using npm
npm init

让我们从添加 typescript 和测试依赖项开始——mocha 和 chai:

yarn add --dev @types/chai @types/mocha chai mocha ts-node typescript
# or if you're using npm
npm install @types/chai @types/mocha chai mocha ts-node typescript --save-dev

现在让我们添加 Waffleethers

yarn add --dev ethereum-waffle ethers
# or if you're using npm
npm install ethereum-waffle ethers --save-dev

你的项目结构现在应该如下所示:

1.
2├── contracts
3├── package.json
4└── test

2. 智能合约

要开始动态模拟,我们需要一个包含依赖项的智能合约。 别担心,我都准备好了!

这是一个用 Solidity 编写的简单智能合约,其唯一目的是检查我们是否有很多钱。 它使用 ERC20 代币来检查我们是否有足够的代币。 将其放在 ./contracts/AmIRichAlready.sol 中。

1pragma solidity ^0.6.2;
2
3interface IERC20 {
4 function balanceOf(address account) external view returns (uint256);
5}
6
7contract AmIRichAlready {
8 IERC20 private tokenContract;
9 uint public richness = 1000000 * 10 ** 18;
10
11 constructor (IERC20 _tokenContract) public {
12 tokenContract = _tokenContract;
13 }
14
15 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"
9
10import IERC20 from "../build/IERC20.json"
11import AmIRichAlready from "../build/AmIRichAlready.json"
12
13use(solidity)
14
15describe("Am I Rich Already", () => {
16 let mockERC20: Contract
17 let contract: Contract
18 let wallet: Wallet
19
20 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})

我们来把这个测试分解成几个部分:

  1. 我们将我们的模拟 ERC20 合约设置为始终返回 999999 个代币的余额。
  2. 检查 contract.check() 方法是否返回 false

我们已经准备好启动这个家伙了:

一个测试通过

所以测试通过了,但是…… 还有一些改进的空间。 balanceOf() 函数将始终返回 99999。 我们可以通过指定一个钱包来改进它,让函数为该钱包返回一些东西——就像一个真正的合约一样:

1it("如果钱包中的代币少于 1000001,则返回 false", async () => {
2 await mockERC20.mock.balanceOf
3 .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.balanceOf
3 .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.balanceOf
3 .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日

本教程对你有帮助吗?