How to Test Ethereum Smart Contracts
by WorldSpectrum from Pixabay

How to Test Ethereum Smart Contracts

By alexroan | Blockchain Developer | 26 Apr 2020


This article walks through how to test Smart Contracts using Solidity and JavaScript.

Prerequisites: A basic understanding of Blockchain, Ethereum and Javascript.

The full working project code can be found on Github.


The Importance of Software Testing

If you want code to work the way it’s intended to, software testing is paramount.

There are two general types of software test: unit tests and integration tests.

  • Unit tests focus on each function in isolation.
  • Integration tests focus on ensuring multiple parts of the code work together as expected.

Blockchain software is no different. It might be argued that Blockchain applications require more emphasis on testing, due to immutability.


Blockchain Testing

Truffle suite gives us two avenues for testing solidity smart contracts: solidity tests and JavaScript tests. The question is, which should we use?

The answer is both.

Figure 1: Test structure diagram

Solidity tests

Writing tests in Solidity gives us the ability to run Blockchain layer tests. They allow tests to call contracts and functions as if they were on the Blockchain themselves. To test the internal behaviour of smart contracts we can:

  • Write unit tests to check function return values and state variable values.
  • Write integration tests that test the interactions between contracts. These ensure that mechanisms such as inheritance and dependency injection are functioning as expected.

JavaScript tests

We also need to make sure that smart contracts exhibit the right external behaviour. To test smart contracts from outside the Blockchain, we use Web3js, just as our DApp would. We need to have confidence that our DApp front end will work properly when calling the smart contracts. These fall under integration tests.


Example Project

The full working project code can be found on Github

We have two smart contracts: Background and EntryPoint.

Background is an internal contract that our DApp front-end doesn’t interact with. EntryPoint is the contract which is designed for our DApp to interact with. It references Background in its code.

The smart contracts

pragma solidity >=0.5.0;

contract Background {
    uint[] private values;

    function storeValue(uint value) public {
        values.push(value);
    }

    function getValue(uint initial) public view returns(uint) {
        return values[initial];
    }

    function getNumberOfValues() public view returns(uint) {
        return values.length;
    }
}

Above, we see our Background contract. It exposes three functions: storeValue(uint)getValue(uint), and getNumberOfValues(). All these functions have simple instructions, so they’re easy to unit test.

pragma solidity >=0.5.0;

import "./Background.sol";

contract EntryPoint {
    address public backgroundAddress;

    constructor(address _background) public{
        backgroundAddress = _background;
    }

    function getBackgroundAddress() public view returns (address) {
        return backgroundAddress;
    }

    function storeTwoValues(uint first, uint second) public {
        Background(backgroundAddress).storeValue(first);
        Background(backgroundAddress).storeValue(second);
    }

    function getNumberOfValues() public view returns (uint) {
        return Background(backgroundAddress).getNumberOfValues();
    }
}

This is our EntryPoint contract. An address for our Background contract is injected into the constructor and used as a state variable, named backgroundAddressEntryPoint exposes three functions: getBackgroundAddress()storeTwoValues(uint, uint), and getNumberOfValues().

storeTwoValues(uint, uint) calls a function in the Background contract twice, so unit testing this function in isolation will prove difficult. The same goes for getNumberOfValues(). These are good cases for integration tests.

Solidity

In Solidity, we’re going to write unit tests and integration tests for our smart contracts. Let’s start with unit tests since they’re the simpler of the two.

Here’s our first unit test: TestBackground:

pragma solidity >=0.5.0;

import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../../../contracts/Background.sol";

contract TestBackground {

    Background public background;

    // Run before every test function
    function beforeEach() public {
        background = new Background();
    }

    // Test that it stores a value correctly
    function testItStoresAValue() public {
        uint value = 5;
        background.storeValue(value);
        uint result = background.getValue(0);
        Assert.equal(result, value, "It should store the correct value");
    }

    // Test that it gets the correct number of values
    function testItGetsCorrectNumberOfValues() public {
        background.storeValue(99);
        uint newSize = background.getNumberOfValues();
        Assert.equal(newSize, 1, "It should increase the size");
    }

    // Test that it stores multiple values correctly
    function testItStoresMultipleValues() public {
        for (uint8 i = 0; i < 10; i++) {
            uint value = i;
            background.storeValue(value);
            uint result = background.getValue(i);
            Assert.equal(result, value, "It should store the correct value for multiple values");
        }
    }
}

It tests our Background contract to make sure it:

  • Stores a new value in its values array.
  • Returns values by their index.
  • Stores multiple values in its values array.
  • Returns the size of its values array.

This is TestEntryPoint, with a unit test called testItHasCorrectBackground() for our EntryPoint contract:

pragma solidity >=0.5.0;

import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../../../contracts/Background.sol";
import "../../../contracts/EntryPoint.sol";

contract TestEntryPoint {

    // Ensure that dependency injection working correctly
    function testItHasCorrectBackground() public {
        Background backgroundTest = new Background();
        EntryPoint entryPoint = new EntryPoint(address(backgroundTest));
        address expected = address(backgroundTest);
        address target = entryPoint.getBackgroundAddress();
        Assert.equal(target, expected, "It should set the correct background");
    }

}

This function tests the dependency injection. As mentioned earlier, the other functions in our EntryPoint contract require interaction with Background so we cannot test them in isolation.

These functions are tested in our integration tests:

pragma solidity >=0.5.0;

import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../../../contracts/Background.sol";
import "../../../contracts/EntryPoint.sol";

contract TestIntegrationEntryPoint {

    BackgroundTest public backgroundTest;
    EntryPoint public entryPoint;

    // Run before every test function
    function beforeEach() public {
        backgroundTest = new BackgroundTest();
        entryPoint = new EntryPoint(address(backgroundTest));
    }

    // Check that storeTwoValues() works correctly.
    // EntryPoint contract should call background.storeValue()
    // so we use our mock extension BackgroundTest contract to
    // check that the integration workds
    function testItStoresTwoValues() public {
        uint value1 = 5;
        uint value2 = 20;
        entryPoint.storeTwoValues(value1, value2);
        uint result1 = backgroundTest.values(0);
        uint result2 = backgroundTest.values(1);
        Assert.equal(result1, value1, "Value 1 should be correct");
        Assert.equal(result2, value2, "Value 2 should be correct");
    }

    // Check that entry point calls our mock extension correctly
    // indicating that the integration between contracts is working
    function testItCallsGetNumberOfValuesFromBackground() public {
        uint result = entryPoint.getNumberOfValues();
        Assert.equal(result, 999, "It should call getNumberOfValues");
    }
}

// Extended from Background because values is private in actual Background
// but we're not testing background in this unit test
contract BackgroundTest is Background {
    uint[] public values;

    function storeValue(uint value) public {
        values.push(value);
    }

    function getNumberOfValues() public view returns(uint) {
        return 999;
    }
}

We can see that TestIntegrationEntryPoint uses an extension of Background called BackgroundTest , defined on line 43, to act as our mock contract. This enables our tests to check if EntryPoint calls the correct functions in the backgroundAddress contract it references.

Javascript test files

In JavaScript, we write an integration test to make sure the contracts act as we expect, so that we can build a DApp which uses them.

Here’s our JavaScript test, entryPoint.test.js:

const EntryPoint = artifacts.require("./EntryPoint.sol");

require('chai')
    .use(require('chai-as-promised'))
    .should();

contract("EntryPoint", accounts => {
    describe("Storing Values", () => {
        it("Stores correctly", async () => {
            const entryPoint = await EntryPoint.deployed();

            let numberOfValues = await entryPoint.getNumberOfValues();
            numberOfValues.toString().should.equal("0");

            await entryPoint.storeTwoValues(2,4);
            numberOfValues = await entryPoint.getNumberOfValues();
            numberOfValues.toString().should.equal("2");
        });
    });
});

Using the functions available in our EntryPoint contract, the JavaScript tests ensure that values from outside the Blockchain can be sent to the smart contract by creating transactions targeting the storeTwoValues(uint, uint) function (line 15). Retrieving the number of values stored on the Blockchain by calling getNumberOfValues() on lines 12 and 16 of the tests ensure that they get stored.


Conclusion

When it comes to testing smart contracts, the more the merrier. There should be no stones left unturned when ensuring all possible paths of execution return expected results. Use Blockchain level Solidity tests for unit tests and integration tests, and use Javascript tests for integration tests at the DApp level.

There are points in this project where more unit or integration tests could have been written, so if you think you can add to this project, by all means, submit a pull request to the repo on Github!


Learn More

If you’re interested in Blockchain Development, I write tutorials, walkthroughs, hints, and tips on how to get started and build a portfolio. Check out this evolving list of Blockchain Development Resources.

If you enjoyed this post and want to learn more about Smart Contract Security, Blockchain Development or the Blockchain Space in general, I highly recommend signing up to the Blockgeeks platform. They have courses on a wide range of topics in the industry, from Coding to Marketing to Trading. It has proven to be an invaluable tool for my development in the Blockchain space.

How do you rate this article?


1

0

alexroan
alexroan

Blockchain Developer


Blockchain Developer
Blockchain Developer

Tutorials, walkthrough, hints and tips on Blockchain Development for all levels of expertise.

Send a $0.01 microtip in crypto to the author, and earn yourself as you read!

20% to author / 80% to me.
We pay the tips from our rewards pool.