了解一下本文所講內(nèi)容:
- 如何用truffle建立測試環(huán)境
- 如何在javascript中編寫測試并在測試網(wǎng)絡中執(zhí)行它們
- 你必須在任何合約中測試的5件事
如何用truffle建立測試環(huán)境
truffle初始化之后,生成的test文件夾,你在javascript或solidity中編寫所有測試的主文件夾。
建議堅持使用javascript編寫測試,因為它是測試合約的最快速、最簡單的方法。
如何編寫測試并在測試網(wǎng)絡中執(zhí)行它們
下面是示例合約:
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/文件夾并創(chuàng)建一個名為todoList.js的文件,第一個字母是小寫,它是一個javascript文件。名稱必須與要測試的合約名稱相同。
在這個測試文件中,首先導入合約合和庫以檢查測試條件。在測試文件中寫下:
const TodoList = artifacts.require('./ TodoList.sol')
const assert = require('assert')
TodoList它只是將智能合約中的代碼轉換為在此使用它的變量。assert是nodejs庫,允許檢查每個測試的條件。
現(xiàn)在,創(chuàng)建一個名為contractInstance的變量。
let contractInstance
在assert初始化下面。合約實例變量將包含稍后將使用的合約實例。
現(xiàn)在創(chuàng)建您將要測試的合約容器:
contract('TodoList', (accounts) => {
})
名稱Todolist只是合約的名稱,可以使用想要的任何文本,因為這只是為了讓你知道當時正在執(zhí)行的內(nèi)容。
添加beforeEach:
contract('TodoList', (accounts) => {
beforeEach (async() => {
contractInstance = await TodoList.deployed();
});
});
該beforeEach函數(shù)將在每次測試之前執(zhí)行,在其中我們只是使用deployed()方法部署新的TodoList合約。
現(xiàn)在可以添加測試了。
每個測試都應驗證某個功能在某些條件下是否正常工作。在這個合約中,有一個叫做addTodo的函數(shù),它只是在notes數(shù)組中添加一個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');
});
})
})
這個測試的步驟細分如下:
- 每個測試都以函數(shù)開始,該函數(shù)it()包含測試的簡短精確描述和回調(diào)函數(shù)。在這種情況下,回調(diào)函數(shù)時async,因為我想使用await修飾符,這將允許我使用更干凈的代碼更輕松地進行測試。如果你不熟悉callbacks、promises、async和await關鍵字,請查看:Callbacks, Promises and Async/Await。web3.toHex()函數(shù)的作用是將文本轉換成十六進制,以便在合約中存儲。
- 然后開始addTodo()的測試。我們希望添加帶有短文本的待辦事項,然后查看它是否實際存儲在智能合約上。await關鍵字允許我等待直到函數(shù)完成添加待辦事項,否則它會在后臺處理時繼續(xù)執(zhí)行代碼。
- 添加note后,檢查智能合約的todos變量,看看note是否在那里。因為todos變量是public的,所以我可以在不使用任何附加函數(shù)的情況下執(zhí)行它。它接收到owner的note和該note的索引(在本例中為0),這是第一個。
- 因為我將note存儲在bytes32類型的變量中,所以可以存儲的文本數(shù)量限制為32字符,并且必須是十六進制文本。因此,當我試圖取回內(nèi)容時,我收到一個十六進制字符串,它由隨機數(shù)和字母組成,如下所示:
0x74686924852857424513218979854654530000000000。實際上用web3.toUtf8()函數(shù)將十六進制轉換為人類可讀文本,并將其存儲在一個名為todoContent的變量中。 - 最后,檢查存儲在智能合約的note內(nèi)容是否正確,因為默認情況下,所有bytes32都有一個null十六進制。當值不相等時,assert.equal()函數(shù)拋出異常,破壞測試。如果它們相等,則測試正確。
接下來,為合約的剩余功能添加測試:
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,
}
}
};
打開終端,轉到項目文件夾并執(zhí)行以下操作啟動測試:
truffle test --localnode
如果測試正確,那么將全部passing。
測試單個文件:
truffle test ./test/todoList.js --localnode
你必須在任何合約測試的5件事
- 始終檢查溢出和下溢。如果要進行任何類型的數(shù)學計算,則必須確保代碼不會溢出或下溢。這些只是意味著你超過了一種unit變量的容量,因此該變量的值重置并嘗試存儲一個巨大的數(shù)字后再次變?yōu)?。
function sumNumbers(uint256 numberA, uint256 numberB) public view returns(uint256) {
return numberA + numberB;
}
// 這個函數(shù)就存在溢出的風險。在這種情況下,可以編寫一個這樣的測試,通過檢查最終值來測試總和是否溢出:
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')
}
})
- 檢查功能的返回值是否始終在預期值的范圍內(nèi)。例如,如果你有一個預期會返回大于0的數(shù)字的函數(shù),請強制返回0的位置進行測試以查看它是否拒絕該情況。
- 始終測試功能的限制。如果一個函數(shù)返回一個數(shù)字,那么寫一個測試,用最大可能的數(shù)字執(zhí)行它,另一個測試用盡可能小的數(shù)字,還有一個測試用中間的隨機值。你永遠不會知道你的功能在意外情況下會如何反應。
- 確保返回值正確格式化。如果有一個應該返回數(shù)字數(shù)組的函數(shù),請檢查該數(shù)組是否返回空的任何情況。這很重要,因為它可能會破壞dapp的功能。
- 確保參數(shù)拒絕無效值。必須確保合約已準備好用于功能參數(shù)的所有可能值,以避免安全風險。
// 比如這樣參數(shù)的函數(shù):
function doSomething (string randomText,uint256 randomNumber) public {}
// 寫一個字符串為空的測試,寫另一個字符串時一萬字的大量文本,寫一個測試,其中unit為零,一個是負數(shù),一個是一個巨大的數(shù)字。