Writing automated smart contract tests

In a blockchain environment, a single mistake could cost you all of your funds - or even worse, your users' funds! This guide will help you develop robust applications by writing automated tests that verify your application behaves exactly as you intended.

We’ll cover the following topics:

Instructions are available for both Truffle and Buidler. Choose your preference using this toggle!

About testing

There is a a wide range of testing techniques, from simple manual verifications to complex end-to-end setups, all of them useful in their own way.

When it comes to smart contract development though, practice has shown that contract unit testing is exceptionally worthwhile. These tests are simple to write and quick to run, and let you add features and fix bugs in your code with confidence.

Smart contract unit testing consists of multiple small, focused tests, which each check a small part of your contract for correctness. They can often be expressed in single sentences that make up a specification, such as 'the admin is able to pause the contract', 'transferring tokens emits an event' or 'non-admins cannot mint new tokens'.

Setting up a testing environment

You may be wondering how we’re going to run these tests, since smart contracts are executed inside a blockchain. Using the actual Ethereum network would be very expensive, and while testnets are free, they are also slow (with blocktimes between 5 and 20 seconds). If we intend to run hundreds of tests whenever we make a change to our code, we need something better.

What we will use is called a local blockchain: a slimmed down version of the real thing, disconnected from the Internet, running on your machine. This will simplify things quite a bit: you won’t need to get Ether, and new blocks will be mined instantly.

Writing unit tests

We’ll use Chai assertions for our unit tests.

$ npm install --save-dev chai

We will keep our test files in a test directory. Tests are best structured by mirroring the contracts directory: for each .sol file there, create a corresponding test file.

Time to write our first tests! These will test properties of the Box contract from previous guides: a simple contract that lets you retrieve a value the owner previously store d.

We will save the test as test/Box.test.js. Each .test.js file should have the tests for a single contract, and be named after it.

// test/Box.test.js
// Load dependencies
const { expect } = require('chai');

// Load compiled artifacts
const Box = artifacts.require('Box');

// Start test block
contract('Box', function () {
  beforeEach(async function () {
    // Deploy a new Box contract for each test
    this.box = await Box.new();
  });

  // Test case
  it('retrieve returns a value previously stored', async function () {
    // Store a value
    await this.box.store(42);

    // Test if the returned value is the same one
    // Note that we need to use strings to compare the 256 bit integers
    expect((await this.box.retrieve()).toString()).to.equal('42');
  });
});

Create a test directory in your project root. We will save the test as test/Box.js. Each test .js file commonly has the tests for a single contract, and is named after it.

// test/Box.js
// Load dependencies
const { expect } = require('chai');

// Start test block
describe('Box', function () {
  before(async function () {
    this.Box = await ethers.getContractFactory("Box");
  });

  beforeEach(async function () {
    this.box = await this.Box.deploy();
    await this.box.deployed();
  });

  // Test case
  it('retrieve returns a value previously stored', async function () {
    // Store a value
    await this.box.store(42);

    // Test if the returned value is the same one
    // Note that we need to use strings to compare the 256 bit integers
    expect((await this.box.retrieve()).toString()).to.equal('42');
  });
});
Many books have been written about how to structure unit tests: for a quick reference, check out the tests for OpenZeppelin Contracts, or the following guide for ERC20 tests.

We are now ready to run our tests!

Running npx truffle test will execute all tests in the test directory, checking that your contracts work the way you meant them to.

Make sure you are running a local blockchain.
$ npx truffle test
Using network 'development'.


Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.



  Contract: Box
    ✓ retrieve returns a value previously stored (71ms)


  1 passing (167ms)

Running npx buidler test will execute all tests in the test directory, checking that your contracts work the way you meant them to:

$ npx buidler test
All contracts have already been compiled, skipping compilation.


  Box
    ✓ retrieve returns a value previously stored


  1 passing (760ms)

It’s also a very good idea at this point to set up a Continuous Integration service such as CircleCI to make your tests run automatically every time you commit your code to GitHub.

Performing complex assertions

Many interesting properties of your contracts may be hard to capture, such as:

  • verifying that the contract reverts on errors

  • measuring by how much an account’s Ether balance changed

  • checking that the proper events are emitted

OpenZeppelin Test Helpers is a library designed to help you test all of these properties. It will also simplify the tasks of simulating time passing on the blockchain and handling very large numbers.

OpenZeppelin Test Helpers is web3.js based, thus Buidler users should use the Truffle plugin for compatibility, or use Waffle with ethers.js, which offers similar functionality.

To install the OpenZeppelin Test Helpers, run:

$ npm install --save-dev @openzeppelin/test-helpers

We can then update our tests to use OpenZeppelin Test Helpers for very large number support, to check for an event being emitted and to check that a transaction reverts.

// test/Box.test.js
// Load dependencies
const { expect } = require('chai');

// Import utilities from Test Helpers
const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');

// Load compiled artifacts
const Box = artifacts.require('Box');

// Start test block
contract('Box', function ([ owner, other ]) {

  // Use large integers ('big numbers')
  const value = new BN('42');

  beforeEach(async function () {
    this.box = await Box.new({ from: owner });
  });

  it('retrieve returns a value previously stored', async function () {
    await this.box.store(value, { from: owner });

    // Use large integer comparisons
    expect(await this.box.retrieve()).to.be.bignumber.equal(value);
  });

  it('store emits an event', async function () {
    const receipt = await this.box.store(value, { from: owner });

    // Test that a ValueChanged event was emitted with the new value
    expectEvent(receipt, 'ValueChanged', { newValue: value });
  });

  it('non owner cannot store a value', async function () {
    // Test a transaction reverts
    await expectRevert(
      this.box.store(value, { from: other }),
      'Ownable: caller is not the owner'
    );
  });
});

These will test properties of the Ownable Box contract from previous guides: a simple contract that lets you retrieve a value the owner previously stored.

Run your tests again to see the Test Helpers in action:

$ npx truffle test
...
  Contract: Box
    ✓ retrieve returns a value previously stored (128ms)
    ✓ store emits an event (69ms)
    ✓ non owner cannot store a value (112ms)


  3 passing (3s)

The Test Helpers will let you write powerful assertions without having to worry about the low-level details of the underlying Ethereum libraries. To learn more about what you can do with them, head to their API reference.

OpenZeppelin Test Environment

An alternative web3.js based test environment is OpenZeppelin Test Environment.

OpenZeppelin Contracts tests use Test Environment.

To install the OpenZeppelin Test Environment, run:

$ npm install --save-dev @openzeppelin/test-environment
Test Environment uses Ganache under the hood, taking care of its configuration for you.

Once you require the library from your JavaScript code, it will automatically run a local testing blockchain for you. It also exports a list of accounts that have been pre-funded with Ether and convenient ways to load your contracts from their compiled artifacts, among other utilities.

const { accounts, contract } = require('@openzeppelin/test-environment');

// Use the different accounts, which are unlocked and funded with Ether
const [ admin, deployer, user ] = accounts;

// Create a contract object from a compilation artifact
const MyContract = contract.fromArtifact('MyContract');

For detailed information on these exported values and their usage, refer to Test Environment’s API reference.

In order to actually run your tests, you will need to also install a JavaScript test runner. You are free to use any of the recommended ones such as Mocha, Jest or Ava: for this guide, we’ll pick Mocha with Chai assertions.

$ npm install --save-dev mocha chai

A Test Environment test using Mocha and Chai looks as follows:

// test/Box.test.js

// Load dependencies
const { accounts, contract } = require('@openzeppelin/test-environment');
const { expect } = require('chai');

// Load compiled artifacts
const Box = contract.fromArtifact('Box');

// Start test block
describe('Box', function () {
  const [ owner ] = accounts;

  beforeEach(async function () {
    // Deploy a new Box contract for each test
    this.contract = await Box.new({ from: owner });
  });

  // Test case
  it('retrieve returns a value previously stored', async function () {
    // Store a value - recall that only the owner account can do this!
    await this.contract.store(42, { from: owner });

    // Test if the returned value is the same one
    // Note that we need to use strings to compare the 256 bit integers
    expect((await this.contract.retrieve()).toString()).to.equal('42');
  });
});

The best way to run our tests is by adding a test script to your package.json:

 "scripts": {
+  "test": "mocha --exit --recursive"
 }

With this, running npm test will execute all tests in the test directory, checking that your contracts work the way you meant them to:

$ npm test
  Box
    ✓ retrieve returns a value previously stored

Don’t forget to re-compile your contracts if you make changes to them! If you want to do this automatically on each test run, set your test script instead to:

truffle compile && mocha --exit --recursive

Test Environment comes with sensible defaults that should work for most use-cases, but you can always configure it to provide more accounts or use different kinds of contract objects, among others.

The OpenZeppelin Test Environment is not required to use the OpenZeppelin Test Helpers: to learn how to use the Test Helpers standalone or integrated in other systems, refer to their documentation.

Next steps

Once you have thoroughly tested your contracts and are reasonably sure of their correctness, you’ll want to deploy them to a real network and start interacting with them. The following guides will get you up to speed on these topics: