12

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

 2 years ago
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.
neoserver,ios ssh client

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



About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK