Whether you develop software yourself or hire developers to build it for you, you expect them to do everything necessary, so you can get functional and bug-free software. Sometimes, however, doing the right thing takes additional time, which is often being skipped due to all the deadlines, budget or some other reason.
In blockchain development as in the traditional software development, unit testing reduces the number of bugs, making it “a must” to effective software development.
In this article, we are going to look at this type of testing – unit testing – and why you should not cut corners and skip this part of the software development cycle.
This is part two of our series of building decentralized domain name system on top of Ethereum, where we’ll get familiar with good practices and techniques during
In the first part, we developed a decentralized application consisted of Ethereum smart contracts, that are acting as a decentralized domain name system. So, at this point, a user should be able to buy, renew, edit and transfer a domain name according to our
To sum up:
- Unit tests are code that tests your contract code
- It executes your contract with predefined function calls (test cases)
- It compares the result of the execution with the expected result for this case
Tooling
The most popular and commonly used way of creating unit tests for Ethereum smart contracts written in Solidity is by using the following tool-set.
- Truffle – is the most popular development framework for Ethereum.
- Built-in smart contract compilation, linking, deployment and binary management.
- Automated contract testing for rapid development.
- Scriptable, extensible deployment & migrations framework.
- Network management for deploying to any number of public & private networks.
- Package management with EthPM & NPM, using the ERC190 standard.
- Interactive console for direct contract communication.
- Configurable build pipeline with support for tight integration.
- External script runner that executes scripts within a Truffle environment.
- Ganache – lets you quickly fire up a personal Ethereum blockchain which you can use to run tests, execute commands, and inspect state while controlling how the chain operates.
Without further due, let’s jump into setting up our project and environment for unit testing.
First if we haven’t already, we need to install Truffle globally (-g)
npm install -g truffle
and Ganache – It is available as both a desktop application as well as a command-line tool (formerly known as the TestRPC). For the purpose of our tutorial we are going to use the command-line tool.
npm install -g ganache-cli
throw
revert()
, require()
and assert()
. So it will be somewhat helpful if you know how the last work.
Nevertheless, let us verify our knowledge by a quick lookup at the Solidity documentation:
Error handling: Assert, Require, Revert and Exceptions
Solidity documentation
Solidity uses state-reverting exceptions to handle errors. Such an exception will undo all changes made to the state in the current call (and all its sub-calls) and also flag an error to the caller. The convenience functionsassert
andrequire
can be used to check for conditions and throw an exception if the condition is not met. Theassert
function should only be used to test for internal errors, and to check invariants. Therequire
function should be used to ensure valid conditions, such as inputs, or contract state variables are met, or to validate return values from calls to external contracts. If used properly, analysis tools can evaluate your contract to identify the conditions and function calls which will reach a failingassert
. Properly functioning code should never reach a failing assert statement; if this happens there is a bug in your contract which you should fix.
There are two other ways to trigger exceptions: Therevert
function can be used to flag an error and revert the current call. It is possible to provide a string message containing details about the error that will be passed back to the caller.
Project initialization
Note: If you have initialized your project with truffle init
you can skip this part.
In the first part we covered mainly the code, but not the environment, IDE and project setup, but for our tests to run properly we will need that set.
Setting up a new truffle project:
truffle init
As you notice after project initialization Truffe adds some files into our project directory:
./contracts
– directory where our smart contracts should be stored../migrations
– directory for placing our migrations scripts../test
– directory for test files for testing our decentralized application../truffle-config.js
– this is our project’s truffle configuration file.
Writing Unit Tests
Writing unit tests is a must for decentralized applications development life-cycle, so we can be sure the application works the way it is designed to.
So Truffle gives us couple of possible options to write tests – in JavaScript, TypeScript (testing your contract via web3) or Solidity (testing your contract from another contract). In our tutorial we will cover unit testing with JavaScript which its the most adopted way of doing so in my opinion.
All contracts in truffle’s contracts folder are accessible and are published automatically for our tests. Our unit tests should be placed in the ./test
folder and must end with .js
extension so it can be recognized as an automated test by Mocha framework which Truffle uses for testing. For the assertions Truffle uses Chai, which you may or may not be familiar with.
Chai is a BDD / TDD assertion library for node and can be paired with any javascript testing framework.
Chaijs
Truffle’s got well structured, extensive documentation which you can read upon and get familiar with, if you want to.
A test is a call of a contract function with predefined (hard-coded) arguments and a predefined expected output. And the expected output should be the correct way the contract functions.
What makes Truffle tests different from that of Mocha is the
Truffle documentationcontract()
function: This function works exactly likedescribe()
except it enables Truffle’s clean-room features.
Each group of tests is ran with clean contract state, meaning that for every test new contract is being published.
Here is how our DDNSService.test.js should look like:
const DDNSService = artifacts.require('../contracts/DDNSService.sol')
// place helpers here
contract('DDNSService', ([owner, wallet, anotherAccount]) => {
let contractInstance;
let events = [];
before(() => {
web3.eth.defaultAccount = owner;
});
beforeEach(async () => {
contractInstance = await DDNSService.new();
});
afterEach(() => {
if (events.length) {
events.forEach((ev) => {
ev.stopWatching();
});
events = [];
}
});
it("BYTES_DEFAULT_VALUE constant Should have exact value", async () => {
// Arrange
// Act
const result = await contractInstance.BYTES_DEFAULT_VALUE();
// Assert
assert.equal(result, '0x00');
});
... more tests
});
In the code above, we’ve set our contract instance and written the first test, which tests whether the BYTES_DEFAULT_VALUE
constant in our smart contract is set to 0x00
.
Here is a more sophisticated example – register Should throw when the sent funds are insufficient – we are getting the price for the passed domain name, IP, TLD, substract 1 via .minus(1)
so we can have a smaller than the actual domain price saved in our currentPrice
constant, which we pass as value argument for the domain register via the register()
function afterwards. Then we assert whether the call failed with assertRevert
.
it("register Should throw when the sent funds are insufficient", async () => {
// Arrange
const domainName = "milenradkov";
const ip = "127.0.0.1";
const topLevelDomain = "com";
const currentPrice = (await contractInstance.getPrice(domainName)).minus(1);
// Act
const result = contractInstance.register(domainName, topLevelDomain, ip, { from: anotherAccount, value: currentPrice });
// Assert
await assertRevert(result);
});
Unit tests in truffle can be written .then
await/async
Testing the contract against the cases we’ve coded is really simple:
1. Start ganache-cli
in separate terminal/cmd – this will instantiate our private blockchain which we are going to use for tests.
ganache-cli
2. Run truffle test
command from the project directory.
truffle test
As you’ve probably noticed already, we are using the so known AAA (Arrange Act Assert) pattern for our unit tests – it’s always good to stick to well-formatted code and best practices when you are coding for lots of well-known reasons.
“Arrange-Act-Assert”a pattern for arranging and formatting code in UnitTest methods:
Each method should group these functional sections, separated by blank lines:
Arrange-Act-Assert pattern unit test
- Arrange all necessary preconditions and inputs.
- Act on the object or method under test.
- Assert that the expected results have occurred.
Helpers
We are going to need some predefined helpers for some of our next test cases, which names are speaking for what they actually do respectively.
//helpers
const assertRevert = require('./utils/assertRevert');
const watchEvent = require('./utils/watchEvent');
const constants = require('./utils/constants');
const increaseTime = require('./utils/increaseTime');
Coverage
Following this logic here are all the test cases covering most of the edge cases of our decentralized domain name service smart contract functions.
Running tests
Conclusion
Unit testing are important when developing smart contracts on Ethereum, as there are lots of vulnerabilities or edge cases that can lead to security flaws or value (ETH) theft.
In the next few parts we will set up code coverage and continuous integration with Travis CI.
Also we will look deeper in to the security part of development and testing, tips and tricks and more, so stay tuned.
Useful links
- Part one – Smart Contracts – Build Decentralized Domain Name System on top of Ethereum
- Part two – Unit Testing – [you are here] 🙂
- Part three – Coming soon …
Website: https://hack.bg
Follow us on social media:
Also published on Medium.