An In-Depth Guide to Testing Ethereum Smart Contracts
A major component in developing smart contracts is testing smart contracts. And yet, for the amount of content written on “how to code in Solidity” there is an unfortunate lack of content about how to write tests for that code.
I think there are several reasons for this:
- This is still a very new ecosystem. Solidity is only 5 years old, Vyper less than 3. While the tooling available today is much greater than what it was just a year ago, it is still far from the state of most mainstream languages.
- Many developers with experience in other languages come from a culture where testing isn’t the norm — as such they may be competent programmers, but lack the knowledge of how to effectively test.
- Smart contracts are deceptively simple. They are limited in syntax and complexity such that it can be easy to be lulled into a false sense of confidence about your code.
So — developers who don’t know what they don’t know are using immature tools to test code that appears so simple, but where a single mistake can mean financial ruin.
In this series of articles we’re going to explore testing Ethereum smart contracts in depth, starting from the absolute basics and working our way into more complex ideas. By the end you should have a moderate understanding of the tools that are available, and an appreciation for what separates a good test suite from a great test suite.
I asked this a lot when I first started coding. Testing brings many benefits to the development process:
- Tests validate behaviors within your smart contract. They give you confidence that your code performs in the ways that you intend, and does not perform in the ways that it should not.
- When developing, tests provide assurance that newly added code does not have unintended side effects. As you add new logic, a passing test suite helps you to confirm that you have not broken any previous functionality.
- A good test suite simplifies refactoring. As you make changes and your tests begin to fail, they provide a clear indicator of which areas still must be addressed.
- Tests save time in debugging. When an unexpected error appears, your test suite allows you to immediately rule out many potential causes. You can often adapt a test to help you reproduce the bug, aiding you in fixing the issue so you can resume development more quickly.
A less tangible benefit, but most important in my mind — learning to write effective tests changes your approach to coding. The process of testing is somewhat akin to attacking your code, and as you hunt for vulnerabilities and edge cases you are developing a more security-focused mindset,which in turn improves the quality of the code that you write.
Similarly, well structured code is much easier to test than spaghetti. Being confronted with the challenge of “how do I test this?” helps you to recognize bad design patterns and learn to write code that is both more organized and more readable.
Types of Tests
Much has been written on different types of tests, how to define them and when to use them. For the purposes of this article we will keep things relatively simple by using two broad categories:
- Unit tests are simple tests that verify a single behavior or component within your code. A well written unit test is quick to execute, and provides a clear picture of what went wrong when it fails.
- Integration tests are more complex tests that validate interactions between multiple components. For smart contract testing this can mean interactions between different components of a single contract, or across multiple contracts.
We’re going to focus on unit testing — integration testing follows many of the same principles, but it is a more complex and nuanced subject that is better approached once we have a handle on the basics of unit testing.
Tools for Testing
You can only test as effectively as the tool set that you are using allows you to. For the purposes of this tutorial I am using the following development tools:
- Brownie, a python framework for developing smart contracts. Python is an excellent choice for testing because of it’s simple, readable syntax. It also has two incredibly powerful testing frameworks — pytest and hypothesis. Brownie extends upon each of these to provide unparalleled capabilities for testing your contracts.
- Ganache, a temporary local blockchain used for testing and development. Ganache provides unlocked, funded accounts and mines new transactions instantly. It also allows actions such as reverting transactions and jumping forward in time, which are essential when testing.
If you wish to follow along with the examples in this article, you should make sure each of these programs is installed and that the version is up-to-date. For help with installation you can check out my other article, “Getting Started With Brownie”.
This tutorial is split into the following articles. When you’re ready, continue on with Part Two: Core Concepts of Testing. See you there!
Part One: Why we Test
Part Two: Core Concepts of Testing
Part Three: Writing Basic Tests
Part Four: Running Your Tests
Part Five: Tools and Techniques for Effective Testing
Part Six: Parametrization and Property-Based Testing
Part Seven: Stateful Testing