Unit Testing Solidity Smart Contracts

Hello, world! I consider unit testing one of the most important techniques and habits to create quality in applications from the very beginning. With the advent of Blockchain and Solidity smart-contracts, I keep the habit and continuously looking for improve it. In particular with Blockchain, I don't think the way to test a smart contract is through deploy it to Ganache, set breakpoints through the code of interest, printf-like debug, and tricks like that. There's nothing wrong with them except that this requires to develop a client application (say, an HTML page with web3.js) and, when testing is against a running Blockchain, it consumes more time and resources. Unit testing works better, is cheaper, and increases productivity while makes the smart contract more resilient to adapts and changes. This article is about using JavaScript to unit test a Solidity smart contract. It assumes you are familiarized with Truffle Framework, the Solidity programming language, Mocha, Chai, and Visual Studio Code. These are the versions of Truffle and Solidity I am using in this article:

PS C:\Blockchain> truffle version
Truffle v5.0.1 (core: 5.0.1)
Solidity v0.5.0 (solc-js)
Node v8.9.3
PS C:\Blockchain>

A simple Solidity smart contract

Let's start by creating a MetaCoin smart contract (one of the truffle boxes):
PS C:\Blockchain> MD MetaCoin
PS C:\Blockchain> CD MetaCoin
PS C:\Blockchain\MetaCoin> truffle unbox MetaCoin
PS C:\Blockchain\MetaCoin>

√ Preparing to download
√ Downloading
√ Cleaning up temporary files
√ Setting up box

Unbox successful. Sweet!

Commands:

  Compile contracts: truffle compile
  Migrate contracts: truffle migrate
  Test contracts:    truffle test

PS C:\Blockchain\MetaCoin>
Do you see the truffle test command as part of the cycle? That's what I am going to do with the MetaCoin smart contract:
pragma solidity >=0.4.25 <0.6.0;

import "./ConvertLib.sol";

// This is just a simple example of a coin-like contract.
// It is not standards compatible and cannot be expected to talk to other
// coin/token contracts. If you want to create a standards-compliant
// token, see: https://github.com/ConsenSys/Tokens. Cheers!

contract MetaCoin {
 mapping (address => uint) balances;

 event Transfer(address indexed _from, address indexed _to, uint256 _value);

 constructor() public {
  balances[tx.origin] = 10000;
 }

 function sendCoin(address receiver, uint amount) public returns(bool sufficient) {
  if (balances[msg.sender] < amount) return false;
  balances[msg.sender] -= amount;
  balances[receiver] += amount;
  emit Transfer(msg.sender, receiver, amount);
  return true;
 }

 function getBalanceInEth(address addr) public view returns(uint){
  return ConvertLib.convert(getBalance(addr),2);
 }

 function getBalance(address addr) public view returns(uint) {
  return balances[addr];
 }
}
As a matter of fact, the box contains unit tests written in JavaScript and Solidity under ./test. For the purposes of this article, I deleted both: the Solidity unit tests file and the JavaScript. I am going to walk you through writing JavaScript unit tests from scratch.

Let's start by creating the MetaCoin smart contract unit tests in ./test/metaCoin.js with the following initial content:
const MetaCoin = artifacts.require("MetaCoin");

contract('MetaCoin', (accounts) => { });
Since the MetaCoin smart contract is the subject under test, it needs to be referenced through const MetaCoin = artifacts.require("MetaCoin");. A smart contract test suite is defined by the contract function (just like the describe function does in typical JavaScript unit testing with Jasmine). Note the (accounts) => { }; function; is currently empty but I will talk about it later. At this point, let's start either Ganache or truffle develop and run the test suite. Although empty, it helps to get in touch with some technicalities I encountered with the truffle metacoin box. It happens that when the truffle test the following error occurs:
PS C:\Blockchain\MetaCoin> truffle test
Error: The network id specified in the truffle config (4447) does not match the one
returned by the network (5777). Ensure that both the network and the provider are
properly configured.
    at detectNetworkId (C:\Users\FranciscoJavier\AppData\Roaming\npm\node_modules\truffle\build\webpack:\packages\truffle-core\lib\environment.js:71:1)
    at <anonymous>
    at process._tickCallback (internal/process/next_tick.js:188:7)
Truffle v5.0.1 (core: 5.0.1)
Node v8.9.3
PS C:\Blockchain\MetaCoin>
This error even happens with the original tests files I deleted but it's not a problem with them or the one just created. It has to do with the truffle-config.js. The unboxed version has the networks element commented out:
module.exports = {
  // Uncommenting the defaults below 
  // provides for an easier quick-start with Ganache.
  // You can also follow this format for other networks;
  // see <http://truffleframework.com/docs/advanced/configuration>
  // for more details on how to specify configuration options!
  /*
  networks: {
    development: {
      host: "127.0.0.1",
      port: 7545,
      network_id: "*"
    },
    test: {
      host: "127.0.0.1",
      port: 7545,
      network_id: "*"
    }
  */
  }
};
By uncommenting it...
module.exports = {
  // Uncommenting the defaults below 
  // provides for an easier quick-start with Ganache.
  // You can also follow this format for other networks;
  // see <http://truffleframework.com/docs/advanced/configuration>
  // for more details on how to specify configuration options!
  networks: {
    development: {
      host: "127.0.0.1",
      port: 7545,
      network_id: "*"
    },
    test: {
      host: "127.0.0.1",
      port: 7545,
      network_id: "*"
    }
  }
};
...the test suite now runs without error:.
PS C:\Blockchain\MetaCoin> truffle test
Using network 'development'.

Compiling .\contracts\ConvertLib.sol...

  0 passing (5ms)

PS C:\Blockchain\MetaCoin>
Back to the test suite and the (accounts) => { };function. The accounts argument holds a reference to the set of the accounts in the Blockchain network testing are targeted; in this case, Ganache. Let's modified the test suite to print them out...
const MetaCoin = artifacts.require("MetaCoin");

contract('MetaCoin', (accounts) => {
    for (var i = 0; i < accounts.length; i++) {
        console.log(`Account[${i}] = ${accounts[i]}`);
    }
});
Test suite output is below:
PS C:\Blockchain\MetaCoin> truffle test
Using network 'development'.

Compiling .\contracts\ConvertLib.sol...
Account[0] = 0xd29cf8Cf94cF21692167f4fA946BC8F47DA45324
Account[1] = 0x38e9b32e96A8D189e7A7E815F5c7D817aAf738E0
Account[2] = 0x07CD88cCB00d257D7a577C214aa1deaCc3AE9364
Account[3] = 0x521842D914F9Cb5145d87294B1388603B4916F57
Account[4] = 0xE7E9ddcf36C18fC919551c83e1286Ae2Ec20FCb0
Account[5] = 0xe70435BC5F5eD77EB5C35D30218Dd30998417fFd
Account[6] = 0x3ce5E55CFee669fF339c45872A4feffdf6dE0fF8
Account[7] = 0xa5721c38A1011bb8B7bB3b9733933743191cFdD2
Account[8] = 0x823f6f292F4934994D9650f1c2278Bc073edcA42
Account[9] = 0xCA8665dc5a34DA561913482fF3BdA398F0512373

  0 passing (2ms)

PS C:\Blockchain\MetaCoin>
Which is, not surprisingly, the same in Ganache:


Or Truffle:
PS C:\Blockchain\MetaCoin> truffle develop
Truffle Develop started at http://127.0.0.1:7545/

Accounts:
(0) 0xd29cf8Cf94cF21692167f4fA946BC8F47DA45324
(1) 0x38e9b32e96A8D189e7A7E815F5c7D817aAf738E0
(2) 0x07CD88cCB00d257D7a577C214aa1deaCc3AE9364
(3) 0x521842D914F9Cb5145d87294B1388603B4916F57
(4) 0xE7E9ddcf36C18fC919551c83e1286Ae2Ec20FCb0
(5) 0xe70435BC5F5eD77EB5C35D30218Dd30998417fFd
(6) 0x3ce5E55CFee669fF339c45872A4feffdf6dE0fF8
(7) 0xa5721c38A1011bb8B7bB3b9733933743191cFdD2
(8) 0x823f6f292F4934994D9650f1c2278Bc073edcA42
(9) 0xCA8665dc5a34DA561913482fF3BdA398F0512373
With those in hand, let's write unit tests to get balances for the first (index [0]) and last account (index [9]) like this:
const MetaCoin = artifacts.require("MetaCoin");

contract('MetaCoin', (accounts) => {
    it('should get a balance of 10000 metacoins for the first account', async () => {
        // arrange
        const instance = await MetaCoin.deployed();

        // act
        const actual = (await instance.getBalance.call(accounts[0])).toNumber();
    
        // assert
        const expected = 10000;
        assert.equal(expected, actual);
    });

    it('should get a balance of 0 metacoins for the last account', async () => {
        // arrange
        const instance = await MetaCoin.deployed();

        // act
        const actual = (await instance.getBalance.call(accounts[9])).toNumber();
    
        // assert
        const expected = 0;
        assert.equal(expected, actual);
    });
});
Our test suite contains now two unit tests running successfully:
PS C:\Blockchain\MetaCoin> truffle test
Using network 'development'.

Compiling .\contracts\ConvertLib.sol...

  Contract: MetaCoin
    √ should get a balance of 10,000 metacoins for the first account (63ms)
    √ should get a balance of 0 metacoins for the last account (78ms)

  2 passing (250ms)

PS C:\Blockchain\MetaCoin>
Once the MetaCoin smart contract has been deployed (await MetaCoin.deployed()) we are in position to call the getBalance method and verify that indeed accounts of interest have been initialized as intended. Now, let's check if metacoins transfer works:
const MetaCoin = artifacts.require("MetaCoin");

contract('MetaCoin', (accounts) => {
    it('should get a balance of 10000 metacoins for the first account', async () => {
        // arrange
        const instance = await MetaCoin.deployed();

        // act
        const actual = (await instance.getBalance.call(accounts[0])).toNumber();
    
        // assert
        const expected = 10000;
        assert.equal(expected, actual);
    });

    it('should get a balance of 0 metacoins for the last account', async () => {
        // arrange
        const instance = await MetaCoin.deployed();

        // act
        const actual = (await instance.getBalance.call(accounts[9])).toNumber();
    
        // assert
        const expected = 0;
        assert.equal(expected, actual);
    });

    it('should transfer metacoins between accounts', async () => {
        // arrange
        const instance = await MetaCoin.deployed();
        const acct0 = accounts[0];
        const acct1 = accounts[1];
        const amount = 123;
        let acct0InitialBalance;
        let acct0FinalBalance;
        let acct1InitialBalance;
        let acct1FinalBalance;

        acct0InitialBalance = (await instance.getBalance.call(acct0)).toNumber();
        acct1InitialBalance = (await instance.getBalance.call(acct1)).toNumber();

        // act
        await instance.sendCoin(acct1, amount, { from: acct0 });

        // assert
        acct0FinalBalance = (await instance.getBalance.call(acct0)).toNumber();
        acct1FinalBalance = (await instance.getBalance.call(acct1)).toNumber();

        assert.equal(acct0FinalBalance, acct0InitialBalance - amount);
        assert.equal(acct1FinalBalance, acct1InitialBalance + amount);
    });
});
And it does!
PS C:\Blockchain\MetaCoin> truffle test
Using network 'development'.

Compiling .\contracts\ConvertLib.sol...

  Contract: MetaCoin
    √ should get a balance of 10000 metacoins for the first account (62ms)
    √ should get a balance of 0 metacoins for the last account (93ms)
    √ should transfer metacoins between accounts (579ms)

  3 passing (797ms)

PS C:\Blockchain\MetaCoin>
Well, that's it. As you can see, unit tests are pretty much the same from the Truffle MetaCoin box. In this article I only add a hint on how to fix a possible truffle test error and a light explanation of the (accounts) => { ... } parameter. The main takeaway is that old, well-established practices and disciplines like unit testing are applicable and provide the same benefits to new technologies like Blockchain.

Comments

Post a Comment

Popular posts from this blog

Angular, Azure AD, and Microsoft Graph

Angular and Azure AD