[Part two – Unit Testing] Build a Decentralized Domain Name System (DDNS) on top of Ethereum

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.


Unit Testing Decentralized Domain Name System
Unit Testing Decentralized Domain Name System

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 unit testing process and deployment of a decentralized application (dApp) development lifecycle.

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 dApp’s logic. If you haven’t checked out the first part of our tutorial yet – this is probably the time to do so.

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.

  1. 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.
  2. 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

Okey, now we are good to go. There is a tutorial for using truffle for unit testing in their website here, but throw function used there is now deprecated and replaced by 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 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 functions assert and require can be used to check for conditions and throw an exception if the condition is not met. The assert function should only be used to test for internal errors, and to check invariants. The require 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 failing assert. 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: The revert 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.

Solidity documentation

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
Decentralized Domain Name System - project initialization
Decentralized Domain Name System – project initialization

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.


So starting with the structure of a unit test case.

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 contract() function: This function works exactly like describe() except it enables Truffle’s clean-room features.

Truffle documentation

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 using .then or await/async style, so it’s up to the developer’s preference how to write them.


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:

  1. Arrange all necessary preconditions and inputs.
  2. Act on the object or method under test.
  3. Assert that the expected results have occurred.
Arrange-Act-Assert pattern unit test

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

instantiate ganache-cli
instantiate ganache-cli
ganache-cli running
ganache-cli running
truffle test - Decentralized Domain Name System using Solidity Smart Contracts on top of Ethereum
truffle test – Decentralized Domain Name System using Solidity Smart Contracts on top of Ethereum
Truffle Test passing - Decentralized Domain Name System using Solidity Smart Contracts on top of Ethereum
Truffle Test passing – Decentralized Domain Name System using Solidity Smart Contracts on top of Ethereum

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

Website: https://hack.bg

Follow us on social media:


Also published on Medium.

About the author

CEO

Milen Radkov has experience building and delivering successful complex software systems and projects for big enterprises and small startups. Software developed by him and his colleagues is being used by over 1000+ retail stores today. Milen has also extensive experience in blockchain development and is a well-known figure in Bulgaria’s blockchain ecosystem.

Milen Radkov

Let's build the decentralized future together!

Subscribe for updates from the blog

One comment

Leave a Reply

Your email address will not be published. Required fields are marked *