以太坊智能合约测试的两种方法【Solidity | JavaScript】

Truffle开发框架提供了以太坊智能合约测试的两种方法:区块链级别的 Solidity测试和DApp级别的JavaScript测试。在这个教程中,我们将介绍 这两种以太坊智能合约测试方法的用途、区别与应用场景,并通过一个具体的示例 来学习如何综合利用Solitiy测试用例和JavaScript测试用例对以太坊智能 合约进行单元测试和集成测试。

七种开发语言的以太坊教程: Java | Php | Python | .Net / C# | Golang | Node.JS | Flutter / Dart

1、以太坊智能合约测试概述

作为软件开发者,我们都知道要让代码正常运行,测试是非常 重要的一个环节。基于区块链的去中心化软件也不例外,而且由于区块链的 不可修改特性,测试对于区块链软件来说就更重要了。

总体上来说有两种类型的软件测试:单元测试和集成测试。单元测试 聚焦于单个函数的测试,而集成测试的目标则是确保各部分代码 组合起来也可以按期望的方式运行。

Truffle是应用最广的以太坊智能合约与DApp开发框架,它提供了两种用于 测试以太坊智能合约的方法:Solidity测试和JavaScript测试。问题是, 我们应该选择哪一种方法?

答案是都需要。

以太坊智能合约测试

用Solidity编写智能合约的测试用例让我们可以在区块链层级进行测试。这种测试 用例可以调用合约方法,就像用例部署在区块链里一样。为了测试智能合约的内部 行为,我们可以:

  • 编写Solidity单元测试来检查智能合约函数的返回值以及状态变量的值
  • 编写Solidity集成测试来检查智能合约之间的交互。这些集成测试可以确保像继承或者 依赖注入这样的机制的运行符合预期

我们也需要确保智能合约能够表现出正确的外部行为。为了从区块链 外部测试智能合约,我们在JavaScript测试用例中使用web3.js,就像在 开发DApp时一样。我们需要对DApp前端可以正确调用智能合约建立信心。 这方面的测试属于集成测试。

因此,简单地说,Solidity测试用例主要用于智能合约内部实现逻辑的验证, 可以用于单元测试和集成测试;而JavaScript用例则主要用于智能合约外部行为 的验证,通常用于集成测试。

2、以太坊智能合约测试的示例项目

假设我们有两个以太坊智能合约需要测试:Background和Entrypoint。

Background是一个内部合约,我们的DApp前端不会直接和它交互。EntryPoint 则是专门供DApp交互的智能合约,在EntryPoint合约内部会访问Background合约。

下面是Background合约的solidity代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pragma solidity >=0.5.0;

contract Background {
uint[] private values;

function storeValue(uint value) public {
values.push(value);
}

function getValue(uint initial) public view returns(uint) {
return values[initial];
}

function getNumberOfValues() public view returns(uint) {
return values.length;
}
}

在上面,我们看到Background合约提供了三个函数:

  • storeValue(uint):写入值
  • getValue(uint) :读取值
  • getNumberOfValues():获取值的数量

这三个合约函数都很简单,因此也很容易进行单元测试。

下面是EntryPoint合约的Solidity代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pragma solidity >=0.5.0;

import "./Background.sol";

contract EntryPoint {
address public backgroundAddress;

constructor(address _background) public{
backgroundAddress = _background;
}

function getBackgroundAddress() public view returns (address) {
return backgroundAddress;
}

function storeTwoValues(uint first, uint second) public {
Background(backgroundAddress).storeValue(first);
Background(backgroundAddress).storeValue(second);
}

function getNumberOfValues() public view returns (uint) {
return Background(backgroundAddress).getNumberOfValues();
}
}

在EntryPoint合约的构造函数中,我们注入了Background合约的部署地址,并 将其存入一个状态变量backgroundAddress。EntryPoint合约暴露出三个函数:

  • getBackgroundAddress():返回Background合约的部署地址
  • storeTwoValues(uint, uint):保存两个值
  • getNumberOfValues():返回值的数量

由于storeTwoValues(uint, uint)函数两次调用Background合约中的一个函数,因此 对这个函数进行单元测试比较困难。getNumberOfValues()也有同样的问题,因此这 两个函数更适合进行集成测试。

3、以太坊智能合约测试的Solidity用例

在这一部分,我们学习如何为智能合约编写Solidity单元测试用例和集成测试用例。 让我们先从简单一点的单元测试开始。

下面是TestBackground测试的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
pragma solidity >=0.5.0;

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

contract TestBackground {

Background public background;

// Run before every test function
function beforeEach() public {
background = new Background();
}

// Test that it stores a value correctly
function testItStoresAValue() public {
uint value = 5;
background.storeValue(value);
uint result = background.getValue(0);
Assert.equal(result, value, "It should store the correct value");
}

// Test that it gets the correct number of values
function testItGetsCorrectNumberOfValues() public {
background.storeValue(99);
uint newSize = background.getNumberOfValues();
Assert.equal(newSize, 1, "It should increase the size");
}

// Test that it stores multiple values correctly
function testItStoresMultipleValues() public {
for (uint8 i = 0; i < 10; i++) {
uint value = i;
background.storeValue(value);
uint result = background.getValue(i);
Assert.equal(result, value, "It should store the correct value for multiple values");
}
}
}

这个单元测试的目的是确保Background合约可以:

  • 在values数组中保存新的值
  • 按索引返回values
  • 在values数组中保存多个值
  • 返回values数组的大小

下面的TestEntryPoint测试中包含了一个单元测试testItHasCorrectBackground() 用于验证EntryPoint合约的功能符合预期:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pragma solidity >=0.5.0;

import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../../../contracts/Background.sol";
import "../../../contracts/EntryPoint.sol";

contract TestEntryPoint {

// Ensure that dependency injection working correctly
function testItHasCorrectBackground() public {
Background backgroundTest = new Background();
EntryPoint entryPoint = new EntryPoint(address(backgroundTest));
address expected = address(backgroundTest);
address target = entryPoint.getBackgroundAddress();
Assert.equal(target, expected, "It should set the correct background");
}

}

这个函数对依赖注入进行测试。如前所述,EntryPoint合约中的其他函数需要 与Background合约交互,因此我们没有办法单独测试这些函数,需要在集成测试 中进行验证。下面是集成测试的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
pragma solidity >=0.5.0;

import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../../../contracts/Background.sol";
import "../../../contracts/EntryPoint.sol";

contract TestIntegrationEntryPoint {

BackgroundTest public backgroundTest;
EntryPoint public entryPoint;

// Run before every test function
function beforeEach() public {
backgroundTest = new BackgroundTest();
entryPoint = new EntryPoint(address(backgroundTest));
}

// Check that storeTwoValues() works correctly.
// EntryPoint contract should call background.storeValue()
// so we use our mock extension BackgroundTest contract to
// check that the integration workds
function testItStoresTwoValues() public {
uint value1 = 5;
uint value2 = 20;
entryPoint.storeTwoValues(value1, value2);
uint result1 = backgroundTest.values(0);
uint result2 = backgroundTest.values(1);
Assert.equal(result1, value1, "Value 1 should be correct");
Assert.equal(result2, value2, "Value 2 should be correct");
}

// Check that entry point calls our mock extension correctly
// indicating that the integration between contracts is working
function testItCallsGetNumberOfValuesFromBackground() public {
uint result = entryPoint.getNumberOfValues();
Assert.equal(result, 999, "It should call getNumberOfValues");
}
}

// Extended from Background because values is private in actual Background
// but we're not testing background in this unit test
contract BackgroundTest is Background {
uint[] public values;

function storeValue(uint value) public {
values.push(value);
}

function getNumberOfValues() public view returns(uint) {
return 999;
}
}

我们可以看到TestIntegrationEntryPoint使用了一个Background的扩展,即定义在第43行的 BackgroundTest,以其作为我们的模拟合约,这可以让我们的测试用例检查EntryPoint 是否调用了部署在backgroundAddress地址处的合约的正确的函数。

4、以太坊智能合约测试的JavaScript用例

我们用JavaScript编写集成测试来确保合约的外部行为满足预期要求,这样我们 就有信息基于这些智能合约开发DApp了。

下面是我们的JavaScript测试文件entryPoint.test.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const EntryPoint = artifacts.require("./EntryPoint.sol");

require('chai')
.use(require('chai-as-promised'))
.should();

contract("EntryPoint", accounts => {
describe("Storing Values", () => {
it("Stores correctly", async () => {
const entryPoint = await EntryPoint.deployed();

let numberOfValues = await entryPoint.getNumberOfValues();
numberOfValues.toString().should.equal("0");

await entryPoint.storeTwoValues(2,4);
numberOfValues = await entryPoint.getNumberOfValues();
numberOfValues.toString().should.equal("2");
});
});
});

使用EntryPoint合约中的函数,JavaScript测试可以确保我们可以将 区块链外部的值利用交易传入智能合约,这是通过调用合约的storeTwoValues(uint,uint) 函数(第15行)实现的。

5、以太坊智能合约测试教程小节

当谈到智能合约的测试时,可以说越多越好,应当尽可能覆盖所有可能的 执行路径都返回预期的结果。Truffle提供了两种办法:区块链层的Solidity单元测试 和集成测试,以及DApp级别的JavaScript集成测试,我们在实际的工作中需要 根据智能合约的代码实现综合运用这两种测试方法来保证智能合约的运行符合预期。


原文链接:How to Test Ethereum Smart Contracts

汇智网翻译整理,转载请标明出处