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

练习3:测试覆盖率

> * 原文:https://stermi.medium.com/how-to-write-your-first-decentralized-app-scaffold-eth-challenge-1-staking-dapp-b0b6a6f4d242 > * 译文出自:[登链翻译计划](https://github.com/lbc-team/Pioneer) > * 译者:[翻译小组](https://learnblockchain.cn/people/412) > * 校对:[Tiny 熊](https://learnblockchain.cn/people/15) > * 本文永久链接:[learnblockchain.cn/article…](https://learnblockchain.cn/article/3191) 我知道,你想直接部署合约和前端,并立刻就开始在测试网上进行测试,但是......我们需要确定一切都按预期工作,而不需要在前端用户界面(UI) 上进行 monkey 测试。 因此,在文章的下一部分,我将介绍一些开发人员应该做的事情:测试合约逻辑! ### Waffle [Waffle](https://ethereum-waffle.readthedocs.io/en/latest/index.html)是一个用于编写和测试智能合约的库,它与 ethers-js 配合得非常默契。 > Waffle 有很多有帮助的工具。waffle 中的测试是用[Mocha](https://mochajs.org/)和[Chai](https://www.chaijs.com/)一起编写的。你可以使用不同的测试环境,但 Waffle 的匹配器(matcher)只能在`chai`下工作。 我们将使用[Chai 匹配器](https://ethereum-waffle.readthedocs.io/en/latest/matchers.html)来验证我们所期望的条件是否已经满足。 在写完所有的测试用例后,你只需要输入`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)获取资金(这也是一种交易)。好吧,为了解决这个问题,我写了一个小程序(你可以直接复制到其他项目中)。 ![](https://img.learnblockchain.cn/pics/20210909180645.png) 当你调用`increaseWorldTimeInSeconds(10, true)`时,EVM 内部时间戳会比当前时间快进10秒。之后,如果指定出块,它还会挖一个块来创建一个交易。 下次合约被调用时,`timeLeft()`应该被更新。 ### 测试execute()函数 我们先看这一部分测试,然后我将发布整段代码,我只解释其中一些特定的代码。这段代码涵盖了 `execute() `函数: ```js 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](https://img.learnblockchain.cn/pics/20210909180747.png) ### 完整测试代码 下面我们来看看整个测试代码: ```js 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...

  • 原文:https://stermi.medium.com/how-to-write-your-first-decentralized-app-scaffold-eth-challenge-1-staking-dapp-b0b6a6f4d242
  • 译文出自:登链翻译计划
  • 译者:翻译小组
  • 校对:Tiny 熊
  • 本文永久链接:learnblockchain.cn/article…

我知道,你想直接部署合约和前端,并立刻就开始在测试网上进行测试,但是......我们需要确定一切都按预期工作,而不需要在前端用户界面(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(examp...

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

  • 单篇购买 5学分
  • 永久订阅专栏 (20学分)
  • 发表于 2021-11-09 15:06
  • 阅读 ( 262 )
  • 学分 ( 0 )
  • 分类:DApp
  • 专栏:从 scaffold-eth 开启 Web3 开发之旅

评论