Testing Smart Contracts
Thursday, 9 July 2020 · 38 min read · ethereum solidityUnit testing is a critical part of smart contract development. The high stakes and rigid immutability of smart contracts demands even more emphasis on testing compared to traditional software. Unit tests ensures your contracts are performing correctly at the most fundamental level, acting as a vanguard in your defense against bugs.
In this article, we’ll learn:
- Why unit testing is important for smart contracts,
- How to write unit tests for smart contracts,
- How to use static types and Typescript to test smart contracts,
- Helpful tools and utilities you can use for complex assertions,
- Other smart contract testing best practices.
New (17 August): Typescript & Typechain support!
📬 Get updates straight to your inbox.
Subscribe to my newsletter so you don't miss new content.
Why Unit Testing is Important for Smart Contracts
Reason 1: Smart Contracts are High Risk software
On a blockchain, a single mistake could cost you all of your funds - or worse, your users’ funds. The cost of failure are in the high tens or hundreds of millions of dollars! 💰🔥
Smart contract bugs can lead to extreme financial losses. The DAO Hack lost more than 11.5 million Ether (US$70M at the time of the hack, now over $2B) and the 2nd Parity Hack lost US$200M of user funds. Today, with a TVL of over $2B, the DeFi space has a lot to lose should things go wrong. Bugs and hacks continue to affect major DeFi protocols today.
The goal of unit testing is to isolate each part of the program and show that the individual parts are correct. A unit test provides a strict, written contract that the piece of code must satisfy. They help you identify edge cases and unexpected behaviour throughout the development cycle.
Some of the major smart contract hacks may have been prevented with more comprehensive testing. A comprehensive test suite helps to reduce and manage the risks associated with smart contract bugs.
Reason 2: Smart Contracts are Immutable
Software has an inherent need for evolvability in response to changing requirements, and smart contract systems are no different. Code will need to be changed if errors are discovered or if improvements need to be made. It is no good to discover a bug, but have no way to deal with it.
Unfortunately, upgrading smart contracts is difficult, especially so for critical components. Smart contracts are immutable by default. This means it’s impossible to upgrade the source code of an already deployed contract. Finding even a tiny bug means you need to redeploy your smart contracts, so the consequences of an error are huge. In this sense, developing smart contracts is closer to hardware programming than web development. There are no easy upgrade paths for smart contracts.
Upgradeability mechanisms such as proxies do exist, but not all smart contract systems use them. Reasons for this include:
- To preserve the immutable aspect of smart contracts, and
- To minimize the need for custody and governance.
In either case, initiating contract upgrades is not a trivial matter. Large-scale upgrades require community consensus and are therefore avoided unless absolutely necessary.
Catching potential bugs before launch reduces the need for an upgrade path. Unit tests ensures your code is performing correctly at the most fundamental level, acting as a critical first line of defense.
Summary: Yes, Unit Testing is Important
In short, the importance of good unit testing practices for smart contracts cannot be overstated. The high stakes when smart contracts go wrong and their limited upgradability forces us to take a more rigorous approach compared to traditional software.
Unit testing plays a critical role in the smart contract development process. Unit tests are simple to write and quick to run, and let you add features and fix bugs in your contracts with confidence. No moving fast and breaking things here!
Hands-On: Getting Started with Writing Unit Tests
I’ve published a testing-solidity
Truffle project that contains the contracts, tests, and utilities we’ll use in the rest of this article.
The Github repo is available here. It’s open source and publicly available for your learning. Star the repo if you found it helpful! 💫
To follow along, just clone and install:
For the rest of this article, we’ll write tests for this Counter
smart contract:
This Counter
contract:
- Has two functions
publish()
andread()
. - Only whitelisted Publishers can
publish()
, and only once every hour. - Anyone can
read()
.
Let’s get started!
Writing Unit Tests
When you write unit tests for a Solidity smart contract, you test with its contract abstractions in Javascript. You don’t actually write tests in Solidity.
Truffle uses Mocha and Chai to provide you with a solid framework from which to write your contract tests in Javascript (or Typescript.) Here’s an example test suite:
A contract()
and it()
clause wraps a test suite and a test case respectively. Both contract()
and it()
callbacks are run in the order they are defined (from top to bottom.)
Mocha provides the hooks before()
, beforeEach()
, after()
, and afterEach()
. These should be used to set up preconditions and clean up after your tests. For us, we use beforeEach
to deploy the Counter
contract we want to test. Note that test contracts are only deployed to a local chain and cleaned at the end of execution.
Tests can appear before, after, or interspersed with your hooks. Hooks will run in the order they are defined, as appropriate; all
before()
hooks run (once), then anybeforeEach()
hooks, tests, anyafterEach()
hooks, and finallyafter()
hooks (once).
Truffle comes preconfigured with the Chai assertion library, which comes in two flavours:
expect
which uses the BDD style, andassert
which provides the classic assert-dot notation, similar to that packaged with Node.js.
Let’s look at what’s happening in our first test case.
Our First Unit Test
In this test case:
- First, we deploy our
Counter
contract in abeforeEach()
clause. - Within the
it()
clause, we check that thevalue
returned fromCounter.read()
is equal to the value passed in during deployment.
Run truffle test
, and you’ll get a summary of your unit tests.
Typescript Support Included!
Did you notice that we have static types in our test? We use Typescript and typechain to generate types for our contracts.
This works with any IDE supporting Typescript. You will never call a non-existent method again!
TypeChain is code generator - it takes in your contract ABIs and generates Typescript classes based on it. We run typechain
every time we recompile our contracts:
Running yarn test
will:
- Call
truffle compile
to recompile your contracts and generate ABIs, and - Call
typechain
to generate classes and types based on the latest ABIs.
Then, you can reference these types in your tests:
Awesome!
Testing Protected Functions
Our example Counter
contract uses an AccessControl
scheme where only whitelisted Publishers are allowed to call publish()
. We want to write unit tests to make sure the following holds true:
- Only addresses with the Publisher role can call
publish()
successfully. - Calls from addresses without the Publisher role should revert.
To test contracts as different accounts, Truffle provides an accounts
array which contains 10 addresses by default:
We can override any contract call with a { from }
argument to send transactions as that address:
Testing Reverts
In the above test case, we call publish()
from the other
address who is not a publisher. We use expectRevert
from the @openzeppelin/test-helpers
library to assert that the transaction reverts.
Testing Events
In our next test case, we call publish
from a publisher address:
We use expectEvent
from the @openzeppelin/test-helpers
library to check that the proper Published
event is emitted.
Grouping Common Behaviours
Our example Counter
contract inherits from an AccessControl
contract. We want to assert that any inherited behaviour still behave as expected in Counter
. After all, functions could be overriden anywhere in the inheritance tree.
A modular way to test inherited behaviour is define a separate AccessControl.behaviour.ts
test suite:
We can use this in the Counter
test suite as shouldBehaveLikeAccessControl
:
Splitting common behaviour into behaviour-specific test suites gives us a modular way to verify inherited behaviour across multiple smart contracts!
How to Time Travel ⏳🏃♂️💨
Smart contracts can implement time-related logic such as cooldowns and rounds. Our Counter
requires that an hour has passed between calls to publish()
:
Obviously, we’re not going to stick around or sleep for an hour in our tests! We can time
travel with our test helpers:
The time
module from the @openzeppelin/test-helpers
library lets you move forward in time or to specific blocks.
Calculating Test Coverage
You can use solidity-coverage
to calculate the test coverage of your unit tests. This plugin is included in our sample project. Run yarn coverage
to generate a report:
Code coverage tools help you spot any untested code. Make sure that all branches are covered!
Testing Gas Usage
You can use eth-gas-reporter
to calculate the gas usage per unit test as well as the average gas usage per method. Here’s an example report:
Gas reports can be helpful for optimizing gas usage. They help you identify the gas-guzzlers amongst your smart contracts.
This plugin is also included for you in the
testing-solidity
sample project. Simply clone and run it! 😀
That’s it! We’ve learned how to unit test smart contracts:
- How to test protected functions by calling as other addresses,
- How to check for reverted transactions,
- How to check for emitted events,
- How to fast forward time,
- How to modularize tests of inherited behaviours,
- How to calculate code coverage, and
- How to get reports on gas usage.
Other Testing Best Practices
Unit tests are an important vanguard in your line of defence against bugs. However, unit testing should not be the only tool in your repertoire! Consider a layered security approach.
Other techniques you can use to help secure smart contracts include static analysis, continuous integration, security audits, and formal verification. Every layer you add enhances the overall security of your system. The more layers, the better.
In Closing
The high stakes and rigid immutability of smart contracts demands even more emphasis on testing compared to traditional software development. Unit tests help verify that your application behaves exactly as you intended. We covered:
- Why unit testing is important for smart contracts,
- How to write unit tests for smart contracts,
- Helpful tools and utilities you can use for complex assertions,
- Other smart contract testing best practices.
Now go forth and build robust smart contract systems!
What’s your smart contract testing setup like? Feel free to comment below with your questions and feedback. 🙂
📬 Get updates straight to your inbox.
Subscribe to my newsletter so you don't miss new content.