scaffold-eth 挑战:测试覆盖率(Part3)

练习3:测试覆盖率

我知道,你想直接部署合约和前端,并立刻就开始在测试网上进行测试,但是......我们需要确定一切都按预期工作,而不需要在前端用户界面(UI) 上进行 monkey 测试。

因此,在文章的下一部分,我将介绍一些开发人员应该做的事情:测试合约逻辑!

Waffle

Waffle是一个用于编写和测试智能合约的库,它与 ethers-js 配合得非常默契。

Waffle 有很多有帮助的工具。waffle 中的测试是用MochaChai一起编写的。你可以使用不同的测试环境,但 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应该会有这样的输出:

1_tjI_7R3lLSq4SI8EeNstFA

完整测试代码

下面我们来看看整个测试代码:


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(examp...

剩余50%的内容订阅专栏后可查看

点赞 0
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
翻译小组
翻译小组
0x9e64...7c84
大家看到好的文章可以在 GitHub 提 Issue: https://github.com/lbc-team/Pioneer/issues 欢迎关注我的 Twitter: https://twitter.com/UpchainDAO