测试以太坊智能合约

了解一下本文所讲内容:

  1. 如何用truffle建立测试环境
  2. 如何在javascript中编写测试并在测试网络中执行它们
  3. 你必须在任何合约中测试的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');
   });
   })
})

这个测试的步骤细分如下:

  1. 每个测试都以函数开始,该函数it()包含测试的简短精确描述和回调函数。在这种情况下,回调函数时async,因为我想使用await修饰符,这将允许我使用更干净的代码更轻松地进行测试。如果你不熟悉callbacks、promises、async和await关键字,请查看:Callbacks, Promises and Async/Await。web3.toHex()函数的作用是将文本转换成十六进制,以便在合约中存储。
  2. 然后开始addTodo()的测试。我们希望添加带有短文本的待办事项,然后查看它是否实际存储在智能合约上。await关键字允许我等待直到函数完成添加待办事项,否则它会在后台处理时继续执行代码。
  3. 添加note后,检查智能合约的todos变量,看看note是否在那里。因为todos变量是public的,所以我可以在不使用任何附加函数的情况下执行它。它接收到owner的note和该note的索引(在本例中为0),这是第一个。
  4. 因为我将note存储在bytes32类型的变量中,所以可以存储的文本数量限制为32字符,并且必须是十六进制文本。因此,当我试图取回内容时,我收到一个十六进制字符串,它由随机数和字母组成,如下所示:0x74686924852857424513218979854654530000000000。实际上用web3.toUtf8()函数将十六进制转换为人类可读文本,并将其存储在一个名为todoContent的变量中。
  5. 最后,检查存储在智能合约的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为零,一个是负数,一个是一个巨大的数字。

参考

  • 测试智能合约的终极指南
  • 如何调试智能合约

你可能感兴趣的:(测试以太坊智能合约)