1. 主页
  2. 文档
  3. 以太坊dApp全栈开发教程...
  4. 基于Truffle搭建
  5. 测试智能合约

测试智能合约

在本系列的早期,我们了解了如何设置 Truffle 并用它来编译,部署Bounties.sol 合约并与其进行交互。

本文将介绍在 Truffle 框架内为智能合约编写测试所需的操作。Truffle 项目中的测试可以用 JavascriptSolidity 编写,本文将重点介绍 Javascript 测试。

Truffle 使用 Mocha 测试框架提供了一种用Javascript 编写测试的简单方法,并使用Chai 进行断言( assertions)。您可以在此处阅读有关Truffle测试 的更多信息。

本教程的源代码在此处 可以找到。

先决条件Prerequisites

NODEJS 7.6+

由于 web3.jstruffle executios 是异步的,我们会使用async /await 来简化测试代码。你必须升级到Node 7.6或更高版本。

TRUFFLE

$ npm install -g truffle

更多详情可参考安装truffle

Truffle 工程

为了测试 Bounties.sol 智能合约,我们需要设置一个truffle 工程来编译和部署。让我们从该系列之前创建的 truffle 工程开始:

$ git clone https://github.com/kauri-io/kauri-fullstack-dapp-tutorial-series.git
$ cd kauri-fullstack-dapp-tutorial-series
$ cp -R truffle-compilation-and-deploy dapp-series-bounties
$ cd dapp-series-bounties

我们还需要安装 truffle-hdwallet-provider 依赖项,以确保项目编译:

$ npm install truffle-hdwallet-provider@web3-one --save

开发区块链: Ganache-CLI

为了部署智能合约,我们需要一个以太坊环境。为此,我们将使用 Ganache-CLI 运行本地开发环境。

注意:如果您用的是Windows,需要先安装Windows开发人员工具

npm install -g windows-build-tools
$ npm install -g ganache-cli

针对windows用户的提醒:

你还需要安装 promise bindings 以避免出错。

npm install mz   
npm install bindings

建立测试文件

现在工程设置已经完成了,下面将创建第一个测试:

  • 首先,我们需要在/test 文件夹中创建一个名为 bounties.js 的文件
  • bounties.js 文件中,我们需要导入Bounties.sol,以便在测试中用到它
const Bounties = artifacts.require("./Bounties.sol");
  • 我们现在将定义一个合约容器,对合约的测试会在容器中运行。通常将其设置为合约的名称,不过这不是必需的,你也可以随你心情起名。
contract('Bounties', function(accounts) {


  let bountiesInstance;


  beforeEach(async () => {
      bountiesInstance = await Bounties.new()
   })
})
  • 在合约容器中,我们还定义了一个变量,以此保存被测试的合约实例 bountiesInstance 和一个 beforeEach 区块
  • beforeEach 区块将在每次测试之前执行,并将部署 Bounties.sol 智能合约的新实例。这可确保每个测试都是在干净的合约状态(a clean contract state)下执行

您的 bounties.js 文件应如下所示:

现在我们有了测试文件的基本框架,可以通过执行以下操作来测试所有设置是否正确:

首先在一个单独的窗口中启动 ganache-cli

$ ganache-cli

接下来,运行 truffle test 命令:

$ truffle test

运行truffle test 会执行truffle项目 /test 文件夹中的所有测试。具体如下:

  1. 编译你的合约
  2. 运行部署合约(migrations )以将合约部署到网络
  3. 针对网络上部署的合约运行测试

编写测试

先看下 issueBounty 函数:

function issueBounty(
  string _data,
  uint64 _deadline
)
  external
  payable
  hasValue()
  validateDeadline(_deadline)
  returns (uint)
{
  bounties.push(Bounty(msg.sender, _deadline, _data, BountyStatus.CREATED, msg.value));
  emit BountyIssued(bounties.length - 1,msg.sender, msg.value, _data);
  return (bounties.length - 1);
}

我们想在这个函数中测试一些东西:

  • happy path:发放奖励(bounty)时应发出 BountyIssued 事件
  • happy path:调用 issueBounty 应该返回一个整数
  • payable 关键字:在不发送值的情况下发放奖励会失败
  • hasValue 修饰符:发出奖励值为0的话会失败
  • validateDeadline 修饰符:发布截止日期不大于现在会失败

辅助函数

我们期望如果输入验证失败,EVM能返回错误。您可以在此处阅读有关Solidity中错误处理的更多信息。

另外,要创建奖励,我们需要传递一个大于EVM当前时间戳的截止日期。

为此,我们需要编写一些辅助函数来帮助我们编写测试:

  • 首先,在 /test 目录下创建一个 utils 文件夹,并创建一个文件 time.js
  • 将以下内容复制到 time.js
function getCurrentTime() {
    return new Promise(function(resolve) {
      web3.eth.getBlock("latest").then(function(block) {
            resolve(block.timestamp)
        });
    })
}


Object.assign(exports, {
  getCurrentTime
});
 

上面的内容使用 web3 库从EVM中获取最新的区块,并返回其时间戳。

  • /test/utils 目录中创建一个名为 assertRevert.js 的文件
  • 将以下内容复制到 assertRevert.js
var assertRevert = async (promise, message) => {
  let noFailureMessage;
  try {
    await promise;


    if (!message) { 
      noFailureMessage = 'Expected revert not received' 
    } else {
      noFailureMessage = message;
    }


    assert.fail();
  } catch (error) {
    if (noFailureMessage) {
      assert.fail(0, 1, message);
    }
    const revertFound = error.message.search('revert') >= 0;
    assert(revertFound, `Expected "revert", got ${error} instead`);
  }
};


Object.assign(exports, {
  assertRevert
}

上面的内容将promise 作为第一个参数,它将是一个web3事务,而断言失败消息assertion fail message )作为下一个。 try语句处理promise,并捕捉错误。如果promise 失败,它会检查错误消息是否包含字符串“revert”。

我们通过将下面几行添加到 bounties.js 测试文件,这样就能导入辅助函数了:

const getCurrentTime = require('./utils/time').getCurrentTime;
const assertRevert = require('./utils/assertRevert').assertRevert;
const dayInSeconds = 86400;

我们还添加了 dayInSeconds 常量,以帮助我们添加天数。

Happy Path

注意:以下所有测试都应放在 bounties.js 文件中

我们的第一个 happy path 的测试看起来像这样:

  it("Should allow a user to issue a new bounty", async () => {
   let time = await getCurrentTime()
   let tx = await bountiesInstance.issueBounty("data",
                               time + (dayInSeconds * 2),
                               {from: accounts[0], value: 500000000000});


   assert.strictEqual(tx.receipt.logs.length, 1, "issueBounty() call did not log 1 event");
   assert.strictEqual(tx.logs.length, 1, "issueBounty() call did not log 1 event");
   const logBountyIssued = tx.logs[0];
   assert.strictEqual(logBountyIssued.event, "BountyIssued", "issueBounty() call did not log event BountyIssued");
   assert.strictEqual(logBountyIssued.args.bounty_id.toNumber(),0, "BountyIssued event logged did not have expected bounty_Id");
   assert.strictEqual(logBountyIssued.args.issuer, accounts[0], "BountyIssued event logged did not have expected issuer");
   assert.strictEqual(logBountyIssued.args.amount.toNumber(),500000000000, "BountyIssued event logged did not have expected amount");


 })

显示内容很多,但很简单:

  • 每个测试都以函数 it() 开始,它将测试的描述作为其第一个参数,并将回调函数作为下一个参数。我们使用 async() 作为回调,因此我们可以使用await
  • 然后使用getCurrentTIme() 作为帮助器(helper),针对bountiesInstance 对象调用issueBounty交易,以确保我们的截止日期有效
  • 该交易从帐户[0]发送,值为500000000000000000
  • 然后我们断言我们的交易收据包含一个记录了1个事件的日志
  • 然后我们断言事件的细节与预期相符

我们测试调用issueBounty (而不是发送交易)的第二个 happy path 如下所示:

 it("Should return an integer when calling issueBounty", async () => {
   let time = await getCurrentTime()
   let result = await bountiesInstance.issueBounty.call("data",
                               time + (dayInSeconds * 2),
                               {from: accounts[0], value: 500000000000});


   assert.strictEqual(result.toNumber(), 0, "issueBounty() call did not return correct id");
 });

在上面我们将.call 添加到issueBounty来调用函数(而不是发出交易)。这将返回函数的返回值,而不是交易接收。

注意:因为结果是 BigNumber 类型,我们还需要在断言函数中调用.toNumber()

Error Path

错误路径(error path)测试是将一个带有无效输入的交易作为断言函数的参数。

为了测试我们的可支付(payable)关键字,我们调用一个没有设置值的交易:

it("Should not allow a user to issue a bounty without sending ETH", async () => {
     let time = await getCurrentTime()
     assertRevert(bountiesInstance.issueBounty("data",
                                 time + (dayInSeconds * 2),
                                 {from: accounts[0]}), "Bounty issued without sending ETH");


   });

要测试 hasValue() 修饰符,我们使用值0调用我们的交易:

it("Should not allow a user to issue a bounty when sending value of 0", async () => {
      let time = await getCurrentTime()
      assertRevert(bountiesInstance.issueBounty("data",
                                  time + (dayInSeconds * 2),
                                  {from: accounts[0], value: 0}), "Bounty issued when sending value of 0");


    });

要测试我们的 validateDeadline 修饰符,我们需要发送两个交易,一个截止日期设置为过去,另一个截止日期设置为现在:

it("Should not allow a user to issue a bounty with a deadline in the past", async () => {
        let time = await getCurrentTime()
        assertRevert(bountiesInstance.issueBounty("data",
                                    time - 1,
                                    {from: accounts[0], value: 0}), "Bounty issued with deadline in the past");


      });


  it("Should not allow a user to issue a bounty with a deadline of now", async () => {
          let time = await getCurrentTime()
          assertRevert(bountiesInstance.issueBounty("data",
                                      time,
                                      {from: accounts[0], value: 0}), "Bounty issued with deadline of now");
        });

现在如果运行truffle test 命令,我们应该看到以下内容:

$ truffle test


Compiling ./contracts/Bounties.sol...
Compiling ./contracts/Migrations.sol...




  Contract: Bounties
    ✓ Should allow a user to issue a new bounty (207ms)
    ✓ Should return an integer when calling issueBounty (142ms)
    ✓ Should not allow a user to issue a bounty without sending ETH (116ms)
    ✓ Should not allow a user to issue a bounty when sending value of 0 (100ms)
    ✓ Should not allow a user to issue a bounty with a deadline in the past (109ms)
    ✓ Should not allow a user to issue a bounty with a deadline of now (110ms)




  6 passing 

Time travel

主要测试中,还有一项是检查截止日期,如果日期已过,则合约不接受履行。为了测试这个,我们需要添加一个辅助函数来提前EVM的时间戳:

/test/utils/time.js 文件中添加以下内容:

function increaseTimeInSeconds(increaseInSeconds) {
    return new Promise(function(resolve) {
        web3.currentProvider.send({
            jsonrpc: "2.0",
            method: "evm_increaseTime",
            params: [increaseInSeconds],
            id: new Date().getTime()
        }, resolve);
    });
};

此函数调用ganache EVM evm_increaseTime RPC 函数,以提前EVM 的区块时间戳。

将新的 increaseTimeInSeconds 函数添加到文件的exports 部分:

Object.assign(exports, {
  increaseTimeInSeconds,
  getCurrentTime
});
 

bounties.js 测试文件中,添加以下内容来导入新辅助函数:

const increaseTimeInSeconds = require('./utils/time').increaseTimeInSeconds;

现在可以在测试中使用它了,如下所示:

  it("Should not allow a user to fulfil an existing bounty where the deadline has passed", async () => {
   let time = await getCurrentTime()
   await bountiesInstance.issueBounty("data",
                     time+ (dayInSeconds * 2),
                     {from: accounts[0], value: 500000000000});


   await increaseTimeInSeconds((dayInSeconds * 2)+1)


   assertRevert(bountiesInstance.fulfillBounty(0,"data",{from: accounts[1]}), "Fulfillment accepted when deadline has passed");


 }

自己试一下

既然您已经了解了如何测试 issueBounty 函数,请尝试为以下函数添加测试:

  • fulfilBounty
  • acceptFulfilment
  • cancelBounty

可阅读源代码以了解更多信息。

标签 , ,

我们要如何帮助您?