

scaffold-eth 挑战:测试覆盖率(Part3)
source link: https://learnblockchain.cn/article/3191
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

我知道,你想直接部署合约和前端,并立刻就开始在测试网上进行测试,但是......我们需要确定一切都按预期工作,而不需要在前端用户界面(UI) 上进行 monkey 测试。
因此,在文章的下一部分,我将介绍一些开发人员应该做的事情:测试合约逻辑!
Waffle
Waffle是一个用于编写和测试智能合约的库,它与 ethers-js 配合得非常默契。
Waffle 有很多有帮助的工具。waffle 中的测试是用Mocha和Chai一起编写的。你可以使用不同的测试环境,但 Waffle 的匹配器(matcher)只能在
chai
下工作。
我们将使用Chai 匹配器来验证我们所期望的条件是否已经满足。
在写完所有的测试用例后,你只需要输入yarn test
,就会自动针对你的合约进行测试。
我不会解释如何使用这个库(你可以简单地看一下下面的代码来了解),我将专注于应该测试什么。
我们的合约已经实现了一些逻辑:
- 用
mapping(address => uint256) public balances
保存用户余额 - 有一个最小质押金额的阀值
uint256 public constant threshold = 1 ether
。 - 有一个最大的时间限制(deadline)
uint256 public deadline = block.timestamp + 120 seconds
。 - 如果外部合约不是
completed
并且deadline
还没有到,用户可以调用stake()
函数 - 如果外部合约不是
completed
并且deadline
还没有到,用户可以调用execute
方法。 - 如果时间已经到了
deadline
并且外部合约不是completed
,用户可以撤回资金。 timeLeft()
返回剩余的秒数,直到时间到deadline
,之后它应该总是返回0
测试中应该涵盖什么
PS: 这是我个人的测试方法,如果你有建议,请在 Twitter 上找我!
我写测试的时候,习惯用一个独立的函数并且覆盖所有边缘情况。试试写一写测试用例来回答下面的问题:
- 是否已经涵盖所有边缘情况?
- 函数是否按预期回退?
- 函数是否按需发出事件?
- 输入特殊值时,函数是否输出预期结果?是否按预期达到新状态?
- 函数是否按预期返回值(如果它有返回)?
如何在测试中模拟挖矿
还记得我们说过吗,为了正确模拟 timeLeft()
,我们必须创建交易或从水龙头(Faucet)获取资金(这也是一种交易)。好吧,为了解决这个问题,我写了一个小程序(你可以直接复制到其他项目中)。
当你调用increaseWorldTimeInSeconds(10, true)
时,EVM 内部时间戳会比当前时间快进10秒。之后,如果指定出块,它还会挖一个块来创建一个交易。
下次合约被调用时,timeLeft()
应该被更新。
测试execute()函数
我们先看这一部分测试,然后我将发布整段代码,我只解释其中一些特定的代码。这段代码涵盖了 execute()
函数:
describe('Test execute() method', () => {
it('execute reverted because stake amount not reached threshold', async () => {
await expect(stakerContract.connect(addr1).execute()).to.be.revertedWith('Threshold not reached');
});
it('execute reverted because external contract already completed', async () => {
const amount = ethers.utils.parseEther('1');
await stakerContract.connect(addr1).stake({
value: amount,
});
await stakerContract.connect(addr1).execute();
await expect(stakerContract.connect(addr1).execute()).to.be.revertedWith('staking process already completed');
});
it('execute reverted because deadline is reached', async () => {
// reach the deadline
await increaseWorldTimeInSeconds(180, true);
await expect(stakerContract.connect(addr1).execute()).to.be.revertedWith('Deadline is already reached');
});
it('external contract sucessfully completed', async () => {
const amount = ethers.utils.parseEther('1');
await stakerContract.connect(addr1).stake({
value: amount,
});
await stakerContract.connect(addr1).execute();
// check that the external contract is completed
const completed = await exampleExternalContract.completed();
expect(completed).to.equal(true);
// check that the external contract has the staked amount in it's balance
const externalContractBalance = await ethers.provider.getBalance(exampleExternalContract.address);
expect(externalContractBalance).to.equal(amount);
// check that the staking contract has 0 balance
const contractBalance = await ethers.provider.getBalance(stakerContract.address);
expect(contractBalance).to.equal(0);
});
});
- 第一个测试:如果在质押金额没有达到阈值的情况下调用
execute()
函数,它将撤销交易并返回适当的错误信息。 - 第二个测试:连续两次调用
execute()
函数,质押已经完成,交易应该被撤销,防止再次调用。 - 第三个测试:在时间到 deadline 之后调用
execute()
函数。交易应该被撤销,因为只能在时间到 deadline 之前调用execute()
函数。 - 最后一个测试:如果所有的要求都满足,那么
execute()
函数不会回退,并且所有都如预期一样。在函数调用外部合约后,completed
变量应该是true
,外部合约balance
应该等于用户的质押金额,我们的合约余额应该等于0
(已经将所有的余额转移到外部合约中)。
如果一切正常,运行yarn test
应该会有这样的输出:
完整测试代码
下面我们来看看整个测试代码:
const {ethers} = require('hardhat');
const {use, expect} = require('chai');
const {solidity} = require('ethereum-waffle');
use(solidity);
// Utilities methods
const increaseWorldTimeInSeconds = async (seconds, mine = false) => {
await ethers.provider.send('evm_increaseTime', [seconds]);
if (mine) {
await ethers.provider.send('evm_mine', []);
}
};
describe('Staker dApp', () => {
let owner;
let addr1;
let addr2;
let addrs;
let stakerContract;
let exampleExternalContract;
let ExampleExternalContractFactory;
beforeEach(async () => {
// Deploy ExampleExternalContract contract
ExampleExternalContractFactory = await ethers.getContractFactory('ExampleExternalContract');
exampleExternalContract = await ExampleExternalContractFactory.deploy();
// Deploy Staker Contract
const StakerContract = await ethers.getContractFactory('Staker');
stakerContract = await StakerContract.deploy(exampleExternalContract.address);
// eslint-disable-next-line no-unused-vars
[owner, addr1, addr2, ...addrs] = await ethers.getSigners();
});
describe('Test contract utils methods', () => {
it('timeLeft() return 0 after deadline', async () => {
await increaseWorldTimeInSeconds(180, true);
const timeLeft = await stakerContract.timeLeft();
expect(timeLeft).to.equal(0);
});
it('timeLeft() return correct timeleft after 10 seconds', async () => {
const secondElapsed = 10;
const timeLeftBefore = await stakerContract.timeLeft();
await increaseWorldTimeInSeconds(secondElapsed, true);
const timeLeftAfter = await stakerContract.timeLeft();
expect(timeLeftAfter).to.equal(timeLeftBefore.sub(secondElapsed));
});
});
describe('Test stake() method', () => {
it('Stake event emitted', async () => {
const amount = ethers.utils.parseEther('0.5');
await expect(
stakerContract.connect(addr1).stake({
value: amount,
}),
)
.to.emit(stakerContract, 'Stake')
.withArgs(addr1.address, amount);
// Check that the contract has the correct amount of ETH we just sent
const contractBalance = await ethers.provider.getBalance(stakerContract.address);
expect(contractBalance).to.equal(amount);
// Check that the contract has stored in our balances state the correct amount
const addr1Balance = await stakerContract.balances(addr1.address);
expect(addr1Balance).to.equal(amount);
});
it('Stake 0.5 ETH from single user', async () => {
const amount = ethers.utils.parseEther('0.5');
const tx = await stakerContract.connect(addr1).stake({
value: amount,
});
await tx.wait();
// Check that the contract has the correct amount of ETH we just sent
const contractBalance = await ethers.provider.getBalance(stakerContract.address);
expect(contractBalance).to.equal(amount);
// Check that the contract has stored in our balances state the correct amount
const addr1Balance = await stakerContract.balances(addr1.address);
expect(addr1Balance).to.equal(amount);
});
it('Stake reverted if deadline is reached', async () => {
// Let deadline be reached
await increaseWorldTimeInSeconds(180, true);
const amount = ethers.utils.parseEther('0.5');
await expect(
stakerContract.connect(addr1).stake({
value: amount,
}),
).to.be.revertedWith('Deadline is already reached');
});
it('Stake reverted if external contract is completed', async () => {
const amount = ethers.utils.parseEther('1');
// Complete the stake process
const txStake = await await stakerContract.connect(addr1).stake({
value: amount,
});
await txStake.wait();
// execute it
const txExecute = await stakerContract.connect(addr1).execute();
await txExecute.wait();
await expect(
stakerContract.connect(addr1).stake({
value: amount,
}),
).to.be.revertedWith('staking process already completed');
});
});
describe('Test execute() method', () => {
it('execute reverted because stake amount not reached threshold', async () => {
await expect(stakerContract.connect(addr1).execute()).to.be.revertedWith('Threshold not reached');
});
it('execute reverted because external contract already completed', async () => {
const amount = ethers.utils.parseEther('1');
await stakerContract.connect(addr1).stake({
value: amount,
});
await stakerContract.connect(addr1).execute();
await expect(stakerContract.connect(addr1).execute()).to.be.revertedWith('staking process already completed');
});
it('execute reverted because deadline is reached', async () => {
// reach the deadline
await increaseWorldTimeInSeconds(180, true);
await expect(stakerContract.connect(addr1).execute()).to.be.revertedWith('Deadline is already reached');
});
it('external contract sucessfully completed', async () => {
const amount = ethers.utils.parseEther('1');
await stakerContract.connect(addr1).stake({
value: amount,
});
await stakerContract.connect(addr1).execute();
// it seems to be a waffle bug see https://github.com/EthWorks/Waffle/issues/469
// test that our Stake Contract has successfully called the external contract's complete function
// expect('complete').to.be.calledOnContract(exampleExternalContract);
// check that the external contract is completed
const completed = await exampleExternalContract.completed();
expect(completed).to.equal(true);
// check that the external contract has the staked amount in it's balance
const externalContractBalance = await ethers.provider.getBalance(exampleExternalContract.address);
expect(externalContractBalance).to.equal(amount);
// check that the staking contract has 0 balance
const contractBalance = await ethers.provider.getBalance(stakerContract.address);
expect(contractBalance).to.equal(0);
});
});
describe('Test withdraw() method', () => {
it('Withdraw reverted if deadline is not reached', async () => {
await expect(stakerContract.connect(addr1).withdraw(addr1.address)).to.be.revertedWith(
'Deadline is not reached yet',
);
});
it('Withdraw reverted if external contract is completed', async () => {
// Complete the stake process
const txStake = await stakerContract.connect(addr1).stake({
value: ethers.utils.parseEther('1'),
});
await txStake.wait();
// execute it
const txExecute = await stakerContract.connect(addr1).execute();
await txExecute.wait();
// Let time pass
await increaseWorldTimeInSeconds(180, true);
await expect(stakerContract.connect(addr1).withdraw(addr1.address)).to.be.revertedWith(
'staking process already completed',
);
});
it('Withdraw reverted if address has no balance', async () => {
// Let time pass
await increaseWorldTimeInSeconds(180, true);
await expect(stakerContract.connect(addr1).withdraw(addr1.address)).to.be.revertedWith(
"You don't have balance to withdraw",
);
});
it('Withdraw success!', async () => {
// Complete the stake process
const amount = ethers.utils.parseEther('1');
const txStake = await stakerContract.connect(addr1).stake({
value: amount,
});
await txStake.wait();
// Let time pass
await increaseWorldTimeInSeconds(180, true);
const txWithdraw = await stakerContract.connect(addr1).withdraw(addr1.address);
await txWithdraw.wait();
// Check that the balance of the contract is 0
const contractBalance = await ethers.provider.getBalance(stakerContract.address);
expect(contractBalance).to.equal(0);
// Check that the balance of the user is +1
await expect(txWithdraw).to.changeEtherBalance(addr1, amount);
});
});
});
你是否注意到,测试代码的覆盖率远远大于合约本身?这就是我们想看到的! 测试所有的东西!
本翻译由 CellETF 赞助支持。
Recommend
-
90
世界范围内密码货币十分火爆,我们观察很多有意思的事情正在发生,直到2008年,我们才有现在的密码货币。比特币出现的原因与原理是什么,到底难...
-
79
说明 之前翻译的一个教程(没有备份原地址,梯子被封了)。原地址找到后补上 正文 zap 提供的字段编码器并一定完全合适自己的需求,比如:希望日志记录的输出和syslog或者其他常见的日志格式类似...
-
9
接下来,我将介绍第一个 scaffold-eth 学习项目:创建一个质押 dApp。 质押dApp是干什么的 这个项目的最终目标是模仿以太坊2.0的质押...
-
5
scaffold-eth 挑战:将合约部署到测试网(Part4)scaffold-eth 挑战:将合约部署到测试网(Part4) 最...
-
4
当你刚开始处理 web3 元数据时,也许会感到不知所措,至少我第一次是这样的感觉。我是那种做不了 YOLO 开发的人,我需要知道我正在做什么,正在用什么,以及如何构建有意义的东西,即使这只是POC(proof of concept)。 YOLO(You only liv...
-
6
正如我们之前所说,这个合约的最终目标是实现一个质押dApp,当满足一些条件,用户就可以质押 ETH。如果没有达到这些条件,用户可以撤回他们的 ETH 。 这些条件是: 至少向质押合约质押1个ETH 在deadline(30秒)内达到1个ETH的质押阈...
-
4
接上一篇:我们创建了一个 ERC20 及使用 ETH 购买Token 的功能。现在我们进一步完善它。 练习3:允许Vendor回购 这是练习的最后一部分,也是最难的一部分,不是从技术的角度,而是从概...
-
5
在之前的scaffold-eth挑战中,我们已经创建了一个dApp。在这个挑战中,将创建一个代币及买卖合约,挑战2 分为两篇:本篇将介绍第一部分创建ERC20代币及如何使用 ETH 购买 Token, 下一篇介绍
-
12
README.md ...
-
3
Istio源码解析系列part3—Mixer工作流程浅析 · Service Mesh|服务网格中文社区 本文系转载,作者:郑伟,小米信息部技术架构组 本系列文章主要从源码(
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK