clueless coding // TODO: be smarter

Unit Testing Internal Functions in Solidity Contracts with Truffle

Unit testing when writing smart contracts in solidity is a really, really big deal. Solidity contracts, when deployed on the blockchain, are often difficult (if not impossible) to change. They also typically handle Ether, which has real, significant monetary value. Keeping both of these ideas in mind, it’s important to go into writing a Ethereum contract with a ‘get it right the first time’ mentality.

This makes solidity testing especially important - arguably moreso than in a different piece of software. Luckily, if you’re using Truffle, there’s a powerful, easy-to-use method to testing your contracts.

Say, for example, we have a contract that contains a member uint256 variable, and a simple setter function to change that variable. It would look something like this:

contract ExampleContract {

  uint256 public uintMember;

  function ExampleContract() public {
    uintMember = 0;
  }


  function changeMember(uint256 input) external {
    uintMember = input;
  }

}

It’s trivial to write a simple test in Truffle:

const IPAdmin = artifacts.require("ExampleContract");

contract("ExampleContract", accounts => {
  it("changes the item with the setter", async () => {
    let instance = await ExampleContract.new();
    instance.changeMember(1);
    let currentMember = await instance.uintMember();
    assert.equal(currentMember, 1);
  });

All we have to do now is run

truffle test

and voila, we’ve tested our setter! Simple as that.

Unfortunately, not all functions are as simple as the one we outlined above. If you notice, changeMember() is an external function. (If you don’t know what that means, you can read up on that here. This made it simple to call from our JavaScript tests - we simply created an instance, and hit the function as if we were a user from the outside.

As our contracts grow, however, not all functions will be external. We might (and probably will) have a contract that looks moreso like this:

contract ExampleContract {

  uint256 public uintMember;

  function ExampleContract() public {
    uintMember = 0;
  }

  function changeMember(uint256 input) internal {
    uintMember = input;
  }

  function incrementMember() external {
    changeMember(uintMember - 1);
  }

  function incrementMember(uint256 input) external {
    changeMember(uintMember - 1);
  }

}

Notice the change from internal to external in our changeMember() function. Restricting functions to their proper scope helps keep your contract safe by keeping people out of code they shouldn’t be in, and helps keep your Solidity organized.

But wait, there’s a hitch! All of a sudden, our original test fails - our JavaScript won’t be recognize that there’s even a function there to call, much less one that we can unit test.

To solve this, a workaround is to create a ‘testable’ version of your contract. For our example contract above, such a version would look something like this:

import './ExampleContract.sol';

contract TestableExampleContract is ExampleContract {

  function test_changeMember(uint256 input) external {
    changeMember(input);
  }

}

Then, like before, we can test this like before:

const IPAdmin = artifacts.require("TestableExampleContract");

contract("TestableExampleContract", accounts => {
  it("changes the item with the setter", async () => {
    let instance = await TestableExampleContract.new();
    instance.changeMember(1);
    let currentMember = await instance.uintMember();
    assert.equal(currentMember, 1);
  });

Of course, there are a couple drawbacks to this approach. For one, we have to write this extra boilerplate contract for the sole sake of ‘proxying’ the function. If we ever edit the function signature, our test contract will break as well. There’s also no true insurance that our internal functions will behave like our testable versions do.

However, given the fact that solidity functions should be rigorously tested, this practice offers a lot of benefits for a little bit of extra work - we don’t have to build out external functions before testing. Instead, we can build small, utility internal functions and build off of those after they’ve been unit tested thoroughly.

Furthermore, the alternative to unit testing these internal functions would be to whitebox test our external functions - something notorious for missing branching paths and being unreliable. Again, because of the nature of solidity contracts, testing internal functions like this pays off many dividends over the development lifecycle.