Back to top
Published on -
August 15, 2018
Written by:
Ann Kilzer

Day 9: The Best of Both Worlds: Smart Contract Unit Testing in Solidity and Javascript

Smart contracts are a powerful tool for creating decentralized apps and custom cryptocurrencies.

Smart contracts are a powerful tool for creating decentralized apps and custom cryptocurrencies. Unlike traditional software, once deployed, they are hard to roll back or patch. And with a market cap of over $200 billion for cryptocurrencies, there’s a lot of value at stake! Unit testing can help us ensure our smart contracts do what we expect.

So how do we write unit tests? It’s now possible to write tests in both Solidity and JavaScript, though the two are not entirely interchangeable. Let’s walk through an example of testing a simple token.  (Simple as in less complex, not necessarily easy!) I put my work in a github repo if you’d like to follow along. You’ll need to install truffle and ganache-cli first. I’m using Truffle v4.1.11, Solidity v0.4.24, Ganache v6.1.6 and running on Mac OS X High Sierra 10.13.6.

My initial token had a constructor like this


pragma solidity 0.4.24;contract Mikancoin {  uint public totalSupply;  mapping (address => uint) public balanceOf;  constructor(uint _initialSupply) public {    totalSupply = _initialSupply;    balanceOf[msg.sender] = _initialSupply;    emit Transfer(0x0, msg.sender, totalSupply);  }...


And migrations/2_deploy_contracts.js looks like this:


var Mikancoin = artifacts.require("Mikancoin");module.exports = function(deployer) {    deployer.deploy(Mikancoin, 100);};


Now we can fire up ganache command line interface:


ganache-cli


In a separate terminal, let’s deploy this to a test blockchain with


truffle compile && truffle migrate


Cool, let’s write our first unit test in Solidity in test/TestMikancoin.sol:


pragma solidity 0.4.24;

import 'truffle/Assert.sol';
import 'truffle/DeployedAddresses.sol';
import '../contracts/Mikancoin.sol';

contract TestMikancoin {
  function testConstructor() public {
    // Get the address and cast it
    Mikancoin mikan = Mikancoin(DeployedAddresses.Mikancoin());
    Assert.equal(mikan.totalSupply(), 100, "Total supply");
    Assert.equal(mikan.balanceOf(this), 0, "We have no Mikan");
  }
}


We can use DeployedAddresses to look up where the Mikancoin was deployed, and cast that address to a Mikancoin. Then we can use our asserts to see if our coin is doing the right thing. Running truffle test shows us that we’ve successfully deployed a coin with 100 Mikan and we have a balance of zero.

Here we reach our first problem. It’s not going to be very interesting -- or thorough -- to write tests if we have no tokens to send.  Who got the initial mikans? Consider that the migration script made the initial deploy, so it’s probably some internal truffle address that got the fruit. Let’s confirm that by using the tool truffle console:

Truffle Console

We can see who called the Mikancoin constructor, and how much gas was spent. We can also see that it matches the first eth address created by ganache-cli. Now we could configure ganache-cli to always generate the same ten addresses, but we still won’t easily be able to control it in solidity. Let’s modify our contract slightly so we can decide who gets the coins on deploy.


 constructor(uint _initialSupply, address _initialOwner) public {
    require(_initialOwner != 0x0);
    totalSupply = _initialSupply;
    balanceOf[_initialOwner] = _initialSupply;
    emit Transfer(0x0, _initialOwner, totalSupply);
  }


We can also write a helper contract to easily manage our Mikancoin.


pragma solidity 0.4.24;

import './Mikancoin.sol';

contract MikanFarm {

  Mikancoin [] public deployed;

  function deployMikancoin(uint _initialSupply) public returns (Mikancoin){
    Mikancoin latest = new Mikancoin(_initialSupply, msg.sender);
    deployed.push(latest);
    return latest;
  }
}



Next, update the migration script:


var MikanFarm = artifacts.require("MikanFarm");

module.exports = function(deployer) {
    deployer.deploy(MikanFarm);
};


And finally, a test where we control the Mikans!


pragma solidity 0.4.24;

import 'truffle/Assert.sol';
import 'truffle/DeployedAddresses.sol';
import '../contracts/MikanFarm.sol';
import '../contracts/Mikancoin.sol';

contract TestMikancoin {

  function testTransfer() public {
    uint startTokens = 100;
    MikanFarm farm = MikanFarm(DeployedAddresses.MikanFarm());
    Mikancoin mikan = farm.deployMikancoin(startTokens);
	Assert.equal(mikan.balanceOf(this), startTokens, "We should have 100 Mikan");
	address fox = 0x284A84baA00626e2773a1138D53923b4acAED2F4;
	Assert.equal(mikan.balanceOf(fox), 0, "Fox has 0 mikan");

	uint tokens	= 7;
	Assert.isTrue(mikan.transfer(fox, tokens), "Transfer should succeed");
	Assert.equal(mikan.balanceOf(fox), tokens, "Fox balance after transfer");
	Assert.equal(mikan.balanceOf(this), startTokens - tokens, "Sender balance after transfer");
  }
}


Now, say we want to test the require statements of transfer and ensure we can’t do something bad like send tokens to the zero address. In solidity, this is difficult, as there is no try-catch. We end up with a really awful stack trace when sending to 0x0:


function testBadTransfer() public {
    MikanFarm farm = MikanFarm(DeployedAddresses.MikanFarm());
    Mikancoin mikan = farm.deployMikancoin(100);
    Assert.isFalse(mikan.transfer(0x0, 5), "Transfer to 0 address should fail"); // require() fails and therefore this will revert
  }


Adding this test and running truffle test generates the following:

Truffle Throw

There used to be ways of testing throws, but since the switch to require & revert, these techniques no longer work. Let’s switch over to javascript to demo testing failure cases. Note you’ll have to run npm install in the root directory before running this test to get the chai dependency.


var Mikancoin = artifacts.require("Mikancoin.sol");

const should = require('chai').should();

contract('Mikancoin', function(accounts) {

  const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';

  beforeEach(async function () {
    this.mikancoin = await Mikancoin.new(300, accounts[0]);
  });

  describe("transfer", function() {
    describe("when the recipient is the zero address", function() {
      const amount = 123;
      it("reverts", async function () {
        try {
          await this.mikancoin.transfer(ZERO_ADDRESS, amount, { from: accounts[0] });
        } catch (error) {
          error.message.should.include('revert', `Expected "revert", got ${error} instead`);
          return;
        }
        should.fail('Revert did not happen');
      });
    });
  });
});


After testing in solidity, javascript feels like a cakewalk. Now run everything with truffle test. Truffle picks up both JavaScript and Solidity test files to give you the best of both worlds:

Truffle Tests Passing

TO BE CONTINUED...