Truffle 框架支持 Solidity和 JavaScript 编写测试用例,本文介绍了他们的区别与应用场景。
阅读本文需要对区块链,以太坊,JavaScript 有所了解。
所有的代码可以在 Github
如果您希望代码按照预期的方式工作,那么软件测试至关重要。
软件测试有两种常规类型:单元测试和集成测试。
区块链软件也不例外。 而且由于不可变性,区块链应用程序需要更多地强调测试。
Truffle 开发框架为我们提供了两种测试Solidity智能合约的途径:Solidity测试和JavaScript测试。 问题是,我们应该使用哪个?
答案是都需要。
用Solidity编写智能合约的测试用例让我们可以在区块链层级进行测试。这种测试用例可以调用合约方法,就像用例部署在区块链里一样。为了测试智能合约的内部行为,我们可以:
我们也需要确保智能合约能够表现出正确的外部行为。为了从区块链外部测试智能合约,我们在JavaScript测试用例中使用web3.js,就像在 开发DApp时一样。我们需要有信心对DApp前端可以正确调用智能合约。 这方面的测试属于集成测试。
我们有两个合约: Background
and EntryPoint
需要测试.
Background
是一个内部合约,DApp前端不会直接和它交互。EntryPoint
则是设计作为供DApp交互的智能合约,在EntryPoint
合约会引用Background
合约。
Background合约代码如下:
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.sol 合约代码如下:
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()
也有同样的问题,因此这两个函数更适合进行集成测试。
在 Solidity 测试用例中,我们将为智能合约编写Solidity单元测试用例和集成测试用例。 让我们先从简单一点的单元测试开始。
TestBackground
测试用例如下:
pragma solidity >=0.5.0;
import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../../../contracts/Background.sol";
contract TestBackground {
Background public background;
// 在每个测试函数之前运行
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
合约的功能符合预期:
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
合约交互,因此我们没有办法单独测试这些函数,需要在集成测试中进行验证。下面是集成测试的代码:
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;
// 在测试用例之前运行
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
地址处的合约。
用JavaScript编写集成测试来确保合约的外部行为满足预期要求,这样我们就可以基于这些智能合约开发DApp了。
下面是我们的JavaScript测试文件 entryPoint.test.js
:
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行)实现的。
通过在测试的第12行和第16行调用getNumberOfValues()
来检索存储在区块链中的值的数量,以确保和存储的值一致。
在测试智能合约时,越多越好。 应该不遗余力确保所有可能的执行路径返回预期结果。 将链级Solidity测试用于单元测试和集成测试,并将Javascript测试用于DApp级别的集成测试。
该项目中有些地方可能要编写更多的单元或集成测试,因此,如果您认为可以添加到该项目中,请向代码库 提交 Pull request。
如果你觉得本文对你用户,下面的文章也可能对你有用:
了解如何使用Truffle将智能合约部署到公共测试网络(英文) .
这里有一篇中文文章介绍使用Truffle将智能合约部署到公共测试网络 .
确保智能合约免受黑客攻击,了解 可重入攻击和所有者逻辑盗窃攻击以及如何防止它们 .
原文链接:https://medium.com/better-programming/how-to-test-ethereum-smart-contracts-35abc8fa199d
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!