使用 Solidity 编写测试脚本

Solidity 测试合约与 JS 测试合约并驾齐驱, 只是以 .sol 为扩展名. 当执行 truffle test 的时候, 每个测试文件都会单独的被引用执行. 这些合约保持与 JS 测试脚本相同的优势, 每个测试都会创建 清洁的沙箱环境, 可以直接访问部署好的合约, 支持导入任何合约依赖. 除了这些功能之外, Truffle 的 Solidity 测试框架创建的时候考虑到了下面的问题:

  • Solidity 测试代码不能使用继承 (例如 Test 合约基类). 这使得测试脚本尽可能的简短, 并且能够极大化的控制测试合约的功能.
  • Solidity 测试代码无需引入第三方断言库. Truffle 提供默认的断言库, 支持在使用的时候修改断言库的参数来满足个性化需求.
  • 可以直接基于 Ethereum 客户端使用 Solidity 测试脚本.

 例子

在进一步深层了解之前, 我们看一个使用 Solidity 进行测试的例子. 这是 truffle unbox metacoin 里面提供的 Solidity 测试的代码:

import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/MetaCoin.sol";

contract TestMetacoin {
  function testInitialBalanceUsingDeployedContract() {
    MetaCoin meta = MetaCoin(DeployedAddresses.MetaCoin());

    uint expected = 10000;

    Assert.equal(meta.getBalance(tx.origin), expected, "Owner should have 10000 MetaCoin initially");
  }

  function testInitialBalanceWithNewMetaCoin() {
    MetaCoin meta = new MetaCoin();

    uint expected = 10000;

    Assert.equal(meta.getBalance(tx.origin), expected, "Owner should have 10000 MetaCoin initially");
  }
}

执行之后将会输出下面的内容:

$ truffle test
Compiling ConvertLib.sol...
Compiling MetaCoin.sol...
Compiling truffle/Assert.sol
Compiling truffle/DeployedAddresses.sol
Compiling ../test/TestMetacoin.sol...

  TestMetacoin
    ✓ testInitialBalanceUsingDeployedContract (61ms)
    ✓ testInitialBalanceWithNewMetaCoin (69ms)

  2 passing (3s)

 测试框架

为了更详细的了解发生了什么, 我们进一步来讨论一下细节:

 断言

truffle/Assert.sol 库提供了类似 Assert.equal() 的断言方法, 这是默认的断言库. 你也可以引入你自己的断言库, 因为 Truffle 集成的断言库与框架是松耦合关系, 只是通过触发断言的事件来实现的. 可以使用 Assert.sol 里面提供的其他断言库.

 部署地址

部署的合约地址(例如通过部署脚本部署的合约), 可以通过 truffle/DeployedAddresses.sol 库获取. 在每个测试开始前, Truffle 通过重新编译连接部署所有的合约来实现一个清洁的沙箱环境. 该库为所有部署好的合约提供如下方法:

DeployedAddresses.<contract name>();

这个方法将会返回用于访问该合约的地址. 参考上面的例子.

为了能使用这些部署好的合约, 必须导入合约代码到测试中. 注意例子中的 import "../contracts/MetaCoin.sol";. 是以相对于当前测试脚本的路径的方式引入进来的, 测试脚本在 ./test 目录下, 引用的是上层文件夹中的内容. 然后便可使用该引入的合约将获取到的地址转化为 MetaCoin 类型.

 测试合约的名字

所有测试合约明星必须以 Test 开头, 是大写的 T 字母, 这将清晰的将测试合约与测试工具或者工程合约区分开来(例如放在 test 文件夹下的合约). 声明哪一个才是真正的测试脚本.

 测试函数的名字

和测试合约名类似, 所有测试方法的名字必须以 test 开头, 小写的 t. 每一个测试方法都会作为一个单独的交易进行, 执行顺序以文件名顺序一致(和js测试一样). truffle/Assert.sol 提供的断言方法将会触发事件, 用作测试结果的评估条件. 断言方法返回一个布尔, 代表断言的输出结果, 可以使用该结果用于判断, 以便于在触发错误之前终止测试. 触发的错误包括但不限于对 Ganache 或者 Truffle 开发环境的影响.

 before / after hooks 钩子方法.

这里有许多测试钩子, 例如: beforeAll, beforeEach, afterAllafterEach 是与 JS 中 Mocha 提供的相同钩子. 可以使用这些钩子来在每一个或者全部测试之前或之后实现设置或者撕毁某些功能. 就像测试方法一样, 每个钩子都是独立的交易. 一些复杂的测试需要预先进行一些配置设定, 这也许会消耗光单个交易的 gas, 解决方法是创建多个不同前缀的钩子, 就像下面例子一样:

import "truffle/Assert.sol";

contract TestHooks {
  uint someValue;

  function beforeEach() {
    someValue = 5;
  }

  function beforeEachAgain() {
    someValue += 1;
  }

  function testSomeValueIsSix() {
    uint expected = 6;

    Assert.equal(someValue, expected, "someValue should have been 6");
  }
}

测试合约也演示了所有的测试分发和钩子方法共享了同一个合约状态. 可以在进行测试之前设置合约数据, 在测试过程中使用该合约数据, 在测试结束后重置, 以便为下一个测试准备环境. 就像JS测试一样, 下一个测试方法将继续使用上一个测试的状态.

 高级功能

Solidity 测试还有一些高级特性, 允许运行特定的测试用例.

 抛出例外

你可以定义合约中应该或者不应该触发例外(例如使用 require()/assert()/revert() 方法, 或者在叫老版本 Solidity 中使用 throw )

本文首先由 Simon de la Rouviere 在 his tutorial Testing for Throws in Truffle Solidity Tests 一文中, 但是文中大量使用已经不推荐的 throw 语法, 在 v0.4.13 之后的版本使用 revert(), require(), 和 assert() 语法替代.

 测试转账交易

你也可以测试合约接收 Ether 的功能, 并且用 Solidity 编写交互脚本. 想要实现这个功能需要用 Solidity 编写一个 public 方法, 该方法返回一个 uint 值, 方法名为 initialBalance. 该参数可以是方法或者是一个属性, 参考下面例子. 当测试合约部署到网络上的时候, Truffle 将会从测试账号自动转入对应的 Ether 到合约之中. 测试合约就可以使用里面的 Ether 进行相关交易了. 注意 initialBalance 是可选的而非必须.

译者注: 下面例子实际定义的是一个 public 属性 initialBalance, 因为定义的 public 属性会自动生成对应同名方法.

import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/MyContract.sol";

contract TestContract {
  // Truffle will send the TestContract one Ether after deploying the contract.
  uint public initialBalance = 1 ether;

  function testInitialBalanceUsingDeployedContract() {
    MyContract myContract = MyContract(DeployedAddresses.MyContract());

    // perform an action which sends value to myContract, then assert.
    myContract.send(...);
  }

  function () {
    // This will NOT be executed when Ether is sent. \o/
  }
}

Truffle 将会自动给合约转账, 使用不会执行回调的方法, 所以你仍然可以在 Solidity 中使用这种高级测试的回调方法,