We will do some real Dapp testing here and see some cool features for doing so. In the previous part of Shark of The Pool Series we have created a fully functional Solidity smart contracts Dapp. Or so we think. Before we can put it into production it has to be tested.

Demo of the Dapp is available on this address.

All source code is freely available in this GitHub repository.

Join us at: Solidity development community to learn more

Testing environment

For the coding part, we will be using Truffle framework with async/await library. Truffle provides web3, Mocha and Chai libraries out of the box. Which is pretty much all we need.

What we also need is a blockchain on which we can safely do stuff. For that we will use Ganache (“testrpc”). What it does, is run a testing blockchain on your machine, and sets up 10 accounts with 100ETH balance each.

Testing

Truffle makes your life super easy here. On truffle init it creates a test directory, where you should put your tests. When you will run truffle test command, it will automatically go through compile, migrate and deploy phase and then run all your test in the directory.

Run truffle test command from your root directory to run tests

$ truffle test

You don’t have to worry whether or not you are testing the latest build. Truffle makes sure of that. If you want to run just a specific test file, you can put it as a parameter of the test command.

To run a single test

$ truffle test ./test/fishToken.js

Testing basics

First we create a test file. This is what a minimalistic test would look like.

const FishToken = artifacts.require("./FishToken.sol")



const timePeriodInSeconds = 3600

const from = Math.floor(new Date() / 1000)

const to = from + timePeriodInSeconds



contract('FishToken', async (accounts) => {

const owner = accounts[0]



let instance

beforeEach('setup contract for each test', async () => {

instance = await FishToken.new(to)

})



it('Owner is shark', async () => {

const currentShark = await instance.currentShark()

assert.equal(currentShark, owner)

})

})

Truffle does some magic here and provides you with web3, Mocha and Chai libraries by default. Let’s see what else we’ve used:

artifacts.require() fetches your contract abstraction. That way there’s no need to require contracts ABI or bytecode.

fetches your contract abstraction. That way there’s no need to require contracts ABI or bytecode. contract() is similar to describe() in Mocha. The only difference is that it gives you accounts parameter, which gets your accounts from web3 and makes your life easier.

is similar to in Mocha. The only difference is that it gives you parameter, which gets your accounts from and makes your life easier. beforeEach() is the same as in Mocha. It gets called before each it() . That way you get a new contract instance for each test.

is the same as in Mocha. It gets called before each . That way you get a new contract instance for each test. Contract.now() deploys a new instance of the contract. Just need to provide constructor parameters.

Each function and variable on the contract can be called from the instance. Lets see how we can call transfer() function on our FishToken contract.

await instance.transfer(to, amount, {from: from, gas: 5000000, gasPrice: '20000000000000'})

transfer() is a function of instance. We need to fill it with the parameters we defined in the contract and a few transaction specific parameters:

to is the address to which we are sending tokens.

is the address to which we are sending tokens. amount is amount of tokens we want to send.

is amount of tokens we want to send. from is optional. By default it is the first account in your accounts variable. This is the account that is seen as msg.sender in the contract code.

is optional. By default it is the first account in your variable. This is the account that is seen as msg.sender in the contract code. gas and gasPrice are both optional. Don’t really have to set them for the testnet, but they set the amount of ether you want to spend for the transactions.

For more code check the tests in the repository. Here I will describe more in general, what kind of tests we have to make for our Dapp. Our Dapp might seem simple, but it actually contains quite a few not so trivial and common things to test.

Visibility

We have to make sure internal functions can’t be executed. By using async/await you can just put try/catch around the statements that should fail, and assert that catch() gets executed.

it("Add to participants should be restricted", async () => {

try {

await instance.addToParticipants(user1)

assert.equal(true, false)

} catch (err) {

assert.equal(true, true)

}

})

We also have to make sure our issueToken() function, can only be executed by the creator, which is a Pool contract.

Overflow and underflow

uint256 max value is 2ˆ256–1. Which is a huge number, but still an overflow and underflow are possible. If you paid attention to the code in the previous part of the series, you could see we’ve already made checks for the overflow and underflow when processing transactions and issuing new tokens.

function transfer(address _to, uint256 _value) public onlyWhileOpen returns (bool success) {

if (balances[msg.sender] < _value || balances[_to] + _value <= balances[_to]) {

return false;

}

In the if statement we check whether the balance of sender is sufficient for this transaction. That by itself prevents underflow. But we also have to check if the balance of receiver and added amount is higher than the current balance. What can happen is that after adding the amount, we get an overflow. That means we cross the highest possible number, and the balance starts to get counted from 0 again. That would most certainly corrupt our data and this checking prevents it.

We need to be careful in every function modifying balance state. In our case that means functions transfer() and issueToken() on the FishToken contract. But also function handling deposits on the Pool contract, have to reject deposit if for whatever reason we can’t issue tokens.

And sure enough, during the testing I found a problem. In case of the overflow, I rejected issuing of the tokens on the FishToken contract, but didn’t handle rejection properly on the Pool contract.

Let’s see the code. This is the old version with the bug.

function () public payable onlyWhileOpen {

require(msg.value > 0);

uint256 rewardTokens = rate.mul(msg.value);

iFishToken(token).issueTokens(msg.sender, rewardTokens);

}

We just call issueTokens() not caring about result. What we should do is require issueTokens() call to succeed, and revert transaction otherwise. Fixed code looks like this

function () public payable onlyWhileOpen {

require(msg.value > 0);

uint256 rewardTokens = rate.mul(msg.value);

require(iFishToken(token).issueTokens(msg.sender, rewardTokens));

}

I have fixed the code in my previous post too now.

Time constrained functions

Testing time constrained functions is a bit tricky. There are two ways to do it. One you can manipulate time of the block on the blockchain. Zeppelin library already has a helper that does that and is free to use. Of course that only works on the testnet.

But what I prefer to do, is creating a mock contract, which lets me manipulate value of the deadline variable. And this is what we will use here. What you do is create a child contract and add functions you need for testing.

pragma solidity ^0.4.21;



import "./SafeMath.sol";

import "./FishToken.sol";



contract TimeMockedFishToken is FishToken {

using SafeMath for uint256;



function leapForwardInTime(uint256 _seconds) public returns (bool success) {

if(deadline.sub(_seconds) > deadline){

return false;

}

deadline = deadline.sub(_seconds);

return true;

}



function leapBackInTime(uint256 _seconds) public returns (bool success) {

if(deadline.add(_seconds) < deadline) {

return false;

}

deadline = deadline.add(_seconds);

return true;

}



function TimeMockedFishToken(uint256 _deadline) FishToken(_deadline) public { }

}

You may find constructor definition a bit weird (I did). All it does is passing parameters to the FishToken constructor.

TimeMockedFishToken has all the methods and variables of the parent FishToken, but let us change deadline value with this two additional methods. See how I checked for underflow and overflow even in this mock contract. Just to get it into the muscle memory!

Debugging

Everyone makes mistakes and sooner or later you will have no idea why some transaction is not getting through. This is where Truffle helps you again. It provides a debugger, which helps you debug transaction execution. You just need a transaction id, and call truffle debug command with it.

Example of the failed transaction.

$ truffle debug 0x1005b31598df0079cb09a591c93246458576298d995d97dd5bf0b7af3cf43804

It will run transaction again and let you go through it step by step. You can also see values of the variables and easily find out what exactly caused transaction to get rejected.

Conclusion

We have finished testing of the smart contracts. I have found a few possible problems and fixed them. I want to mention that I only covered tests applicable to our Dapp. For more I highly recommend going through the list of knows attacks by Consensys. Now we can be fairly confident that our smart contracts work as planned.

Do you find any errors in my code? Do you see a way to optimize it? Please let me know, I would love to hear your feedback!

Demo of the Dapp is available on this address.

All source code is freely available in this GitHub repository.

Parts of the series: