跳转至主要内容

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

waffle智能合约solidity测试模拟
中级
Daniel Izdebski
2020年11月14日
10 分钟阅读 minute read

本教程是关于什么的?

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

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

本文假定:

  • 您已经知道如何在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

让我们从添加类型脚本和测试依赖项开始-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}
复制

现在我们已经准备好使用 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"
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("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})

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

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

我们已经准备好启动这个野兽了:

一次测试通过

所以这个测试是有效的,但是......还是有一些改进的余地。 balanceOf()函数将始终返回 99999。 我们可以通过指定一个钱包来改进它,该函数应该为它返回一些东西 -- 就像一个真正的合约。

1it("returns false if the wallet has less than 1000001 tokens", 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("returns true if the wallet has at least 1000001 tokens", 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("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.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)找到。

您可能还感兴趣的教程:

上次修改时间: @GoForceX(opens in a new tab), 2024年2月26日

本教程对你有帮助吗?