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:
About testing
There is 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 of 12 seconds or more). 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, which are available by installing the Hardhat Toolbox.
$ npm install --save-dev @nomicfoundation/hardhat-toolbox
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.
Create a test
directory in your project root. We will save the test as test/Box.test.js
. Each test .js
file commonly has the tests for a single contract, and is named after it.
// test/Box.test.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.waitForDeployment();
});
// 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. Check out the Moloch Testing Guide for a set of principles designed for testing Solidity smart contracts. |
We are now ready to run our tests!
Running npx hardhat test
will execute all tests in the test
directory, checking that your contracts work the way you meant them to:
$ npx hardhat test
Box
✓ retrieve returns a value previously stored
1 passing (578ms)
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
We recommend using Hardhat Chai Matchers to help you test all of these properties, and Hardhat Network Helpers for simulating time passing on the blockchain. These tools will let you write powerful assertions without having to worry about the low-level details of the underlying Ethereum libraries.