了解一下本文所讲内容:
- 如何用truffle建立测试环境
- 如何在javascript中编写测试并在测试网络中执行它们
- 你必须在任何合约中测试的5件事
如何用truffle建立测试环境
truffle初始化之后,生成的test文件夹,你在javascript或solidity中编写所有测试的主文件夹。
建议坚持使用javascript编写测试,因为它是测试合约的最快速、最简单的方法。
如何编写测试并在测试网络中执行它们
下面是示例合约:
pragma solidity 0.4.20;
contract TodoList {
struct Todo {
uint256 id;
bytes32 content;
address owner;
bool isCompleted;
uint256 timestamp;
}
uint256 public constant maxAmountOfTodos = 100;
// Owner => todos
mapping(address => Todo[maxAmountOfTodos]) public todos;
// Owner => last todo id
mapping(address => uint256) public lastIds;
modifier onlyOwner(address _owner) {
require(msg.sender == _owner);
_;
}
// Add a todo to the list
function addTodo(bytes32 _content) public {
Todo memory myNote = Todo(lastIds[msg.sender], _content, msg.sender, false, now);
todos[msg.sender][lastIds[msg.sender]] = myNote;
if(lastIds[msg.sender] >= maxAmountOfTodos) lastIds[msg.sender] = 0;
else lastIds[msg.sender]++;
}
// Mark a todo as completed
function markTodoAsCompleted(uint256 _todoId) public onlyOwner(todos[msg.sender][_todoId].owner) {
require(_todoId < maxAmountOfTodos);
require(!todos[msg.sender][_todoId].isCompleted);
todos[msg.sender][_todoId].isCompleted = true;
}
}
转到test/文件夹并创建一个名为todoList.js的文件,第一个字母是小写,它是一个javascript文件。名称必须与要测试的合约名称相同。
在这个测试文件中,首先导入合约合和库以检查测试条件。在测试文件中写下:
const TodoList = artifacts.require('./ TodoList.sol')
const assert = require('assert')
TodoList它只是将智能合约中的代码转换为在此使用它的变量。assert是nodejs库,允许检查每个测试的条件。
现在,创建一个名为contractInstance的变量。
let contractInstance
在assert初始化下面。合约实例变量将包含稍后将使用的合约实例。
现在创建您将要测试的合约容器:
contract('TodoList', (accounts) => {
})
名称Todolist只是合约的名称,可以使用想要的任何文本,因为这只是为了让你知道当时正在执行的内容。
添加beforeEach:
contract('TodoList', (accounts) => {
beforeEach (async() => {
contractInstance = await TodoList.deployed();
});
});
该beforeEach函数将在每次测试之前执行,在其中我们只是使用deployed()方法部署新的TodoList合约。
现在可以添加测试了。
每个测试都应验证某个功能在某些条件下是否正常工作。在这个合约中,有一个叫做addTodo的函数,它只是在notes数组中添加一个note。所以下面开始测试:
contract('TodoList', (accounts) => {
beforeEach(async () => {
contractInstance = await TodoList.deployed()
})
it('should add a to-do note successfully with a short text of 20 letters',async() => {
await contractInstance.addTodo(web3.toHex('this is a short text'));
const newAddedTodo = await contractInstance.todos(accounts[0],0);
const todoContent = web3.toUtf8(newAddedTodo[1]);
assert.equal(todoContent, 'this is a short text','The content of the new added todo is not correct');
});
})
})
这个测试的步骤细分如下:
- 每个测试都以函数开始,该函数it()包含测试的简短精确描述和回调函数。在这种情况下,回调函数时async,因为我想使用await修饰符,这将允许我使用更干净的代码更轻松地进行测试。如果你不熟悉callbacks、promises、async和await关键字,请查看:Callbacks, Promises and Async/Await。web3.toHex()函数的作用是将文本转换成十六进制,以便在合约中存储。
- 然后开始addTodo()的测试。我们希望添加带有短文本的待办事项,然后查看它是否实际存储在智能合约上。await关键字允许我等待直到函数完成添加待办事项,否则它会在后台处理时继续执行代码。
- 添加note后,检查智能合约的todos变量,看看note是否在那里。因为todos变量是public的,所以我可以在不使用任何附加函数的情况下执行它。它接收到owner的note和该note的索引(在本例中为0),这是第一个。
- 因为我将note存储在bytes32类型的变量中,所以可以存储的文本数量限制为32字符,并且必须是十六进制文本。因此,当我试图取回内容时,我收到一个十六进制字符串,它由随机数和字母组成,如下所示:
0x74686924852857424513218979854654530000000000
。实际上用web3.toUtf8()函数将十六进制转换为人类可读文本,并将其存储在一个名为todoContent的变量中。 - 最后,检查存储在智能合约的note内容是否正确,因为默认情况下,所有bytes32都有一个null十六进制。当值不相等时,assert.equal()函数抛出异常,破坏测试。如果它们相等,则测试正确。
接下来,为合约的剩余功能添加测试:
it('should mark one of your to-dos as completed', async () => {
await contractInstance.addTodo('example')
await contractInstance.markTodoAsCompleted(0)
const lastTodoAdded = await contractInstance.todos(accounts[0], 0)
const isTodoCompleted = lastTodoAdded[3] // 3 is the bool isCompleted value of the todo note
assert(isTodoCompleted, 'The todo should be true as completed')
})
使用npm安装ganache-cli,(或者直接下载Mac客户端,并打开ganache):
npm i -g ganache-cli
然后在truffle-config.js中配置:
module.exports = {
networks: {
localnode: {
network_id: "5777",
host: "127.0.0.1",
port: 7545,
}
}
};
打开终端,转到项目文件夹并执行以下操作启动测试:
truffle test --localnode
如果测试正确,那么将全部passing
。
测试单个文件:
truffle test ./test/todoList.js --localnode
你必须在任何合约测试的5件事
- 始终检查溢出和下溢。如果要进行任何类型的数学计算,则必须确保代码不会溢出或下溢。这些只是意味着你超过了一种unit变量的容量,因此该变量的值重置并尝试存储一个巨大的数字后再次变为0。
function sumNumbers(uint256 numberA, uint256 numberB) public view returns(uint256) {
return numberA + numberB;
}
// 这个函数就存在溢出的风险。在这种情况下,可以编写一个这样的测试,通过检查最终值来测试总和是否溢出:
it('the sum should not overflow', async () => {
try {
// Trying to sum 2^256 + 5, which should overflow and throw an exception in the best case
const sumResult = contractInstance.sumNumbers(2e256, 5)
assert.ok(false, 'The contract should throw an exception to avoid overflowing and thus making bad calculations')
} catch(error) {
assert.ok(true, 'The contract is throwing which is the expected behaviour when you try to overflow')
}
})
- 检查功能的返回值是否始终在预期值的范围内。例如,如果你有一个预期会返回大于0的数字的函数,请强制返回0的位置进行测试以查看它是否拒绝该情况。
- 始终测试功能的限制。如果一个函数返回一个数字,那么写一个测试,用最大可能的数字执行它,另一个测试用尽可能小的数字,还有一个测试用中间的随机值。你永远不会知道你的功能在意外情况下会如何反应。
- 确保返回值正确格式化。如果有一个应该返回数字数组的函数,请检查该数组是否返回空的任何情况。这很重要,因为它可能会破坏dapp的功能。
- 确保参数拒绝无效值。必须确保合约已准备好用于功能参数的所有可能值,以避免安全风险。
// 比如这样参数的函数:
function doSomething (string randomText,uint256 randomNumber) public {}
// 写一个字符串为空的测试,写另一个字符串时一万字的大量文本,写一个测试,其中unit为零,一个是负数,一个是一个巨大的数字。
参考
- 测试智能合约的终极指南
- 如何调试智能合约