Testing Your Smart Contracts With JavaScript (Including Async/Await Promises)

[ The code for this blog post can be found here: https://github.com/gustavoguimaraes/smart-contract-testing-javascript-example-]

Smart contract developers strive to mitigate bugs in their code as they can be literally costly. For this end, one of the techniques we use is to write tests extensively or dare I say obsessively to the codebase.

In this post I will show you how to start testing your smart contracts from the get go.

Show me the green light to move forward

First off, these are the dependencies I used to create this tutorial. If you come across an error, you can try installing and using these versions.

- node@8.5.0
- truffle@^4.0.0
- ethereumjs-testrpc@^4.0.1

With the dependencies out of the way, let’s set up the project.

mkdir smart-contract-test-example && cd "$_" && truffle init webpack

The snippet above creates a directory, changes into it and initializes Truffle with webpack.

Now create the test file for the FundRaise smart contract we’ll build.

touch test/fundRaise.js

Open the file in your favorite text editor and add the most basic test structure.

const FundRaise = artifacts.require('./FundRaise.sol')
contract('FundRaise', function () {
})

They first line fetches the contract artifacts. It is the contract abstraction containing its primary information i.e. its ABI, address and the like.

Then we create the function contract() which is similar to the describe() function in Mocha, except that Truffle adds some features such as making sure the contracts are deployed before running the tests. By the way, Truffle uses the Mocha testing framework as well as Chai assertion library under the hood.

Now let’s run this barebones test.

First:

testrpc

Then open a new command line window and type

truffle test test/fundRaise.js

The outcome is:

Error: Could not find artifacts for ./FundRaise.sol from any sources
at Resolver.require (/usr/local/lib/node_modules/truffle/build/cli.bundled.js:49072:9)
at TestResolver.require (/usr/local/lib/node_modules/truffle/build/cli.bundled.js:89733:30)
...

This is because we haven’t coded up the FundRaise smart contract just yet. Let’s fix this.

touch contracts/FundRaise.sol

Then add the code

pragma solidity ^0.4.17;
contract FundRaise {
}

Run the test again:

 0 passing (1ms)

Right. No tests written yet.

Smart Contract Specification and Adding Tests

The FundRaise smart contract will be a simple contract. Its straightforward specification is:

  • it has an owner
  • it accepts funds
  • it is able to pause and un-pause the fundraising activity
  • contract owner is able to remove funds from the contract to herself at any time.

Let’s start with the first specification — the contract has a owner

const FundRaise = artifacts.require('./FundRaise.sol')
contract('FundRaise', function ([owner]) {
let fundRaise
    beforeEach('setup contract for each test', async function () {
fundRaise = await FundRaise.new(owner)
})
    it('has an owner', async function () {
assert.equal(await fundRaise.owner(), owner)
})
})

In the test code above we accomplish a few things.

1- fancy ES2015 destructuring variable assignment in function([owner] the first parameter given to the contract function is an array with the accounts coming from testrpc. We are taking the first one and assigning it the variable owner .

2- create the fundRaise variable

3- have a beforeEach function which will run before each test creating a new instance of fundRaise each time. Note the use of async/await for promises. This allows for more readable test code. If you want to read up more on the new JavaScript async/await features, this is a good blog post on it.

4- create the first test within the it() function block. Here we are asserting that the fundRaise.owner() is the owner that we passed to when creating the contract.

Before running the tests once more, head over to truffle.js and require babel-polyfill as we need it to use async/await.

truffle.js

// Allows us to use ES6 in our migrations and tests.
require('babel-register')
require('babel-polyfill')
module.exports = {
networks: {
development: {
host: 'localhost',
port: 8545,
network_id: '*' // Match any network id
}
}
}

Run the tests again and you will find this error:

...
1 failing
1) Contract: FundRaise has an owner:
AssertionError: expected undefined to equal '0x676c48fb3979cf2e47300e8ce80a99087589650d'
...

Now it is time to write the code that will make the first test pass. Let’s flesh out our smart contract a bit.

pragma solidity ^0.4.17;
contract FundRaise {
address public owner;
    // @dev constructor function. Sets contract owner 
function FundRaise() {
owner = msg.sender;
}
}

Trigger the test again, i.e.truffle test test/fundRaise.js :

Contract: FundRaise
✓ has an owner (41ms)
1 passing (138ms)

Great! Let’s move on and add the next one.

const FundRaise = artifacts.require('./FundRaise.sol')
  contract('FundRaise', function ([owner, donor]) {
let fundRaise
    beforeEach('setup contract for each test', async function () {
fundRaise = await FundRaise.new(owner)
})
    it('has an owner', async function () {
assert.equal(await fundRaise.owner(), owner)
})
    it('is able to accept funds', async function () {
await fundRaise.sendTransaction({ value: 1e+18, from: donor })
      const fundRaiseAddress = await fundRaise.address
assert.equal(web3.eth.getBalance(fundRaiseAddress).toNumber(), 1e+18)
})
})

The error this time is:

1 failing
1) Contract: FundRaise is able to accept funds:
Error: VM Exception while processing transaction: invalid opcode

Right, we need to let our contract receive Ether. Let’s fix that.

pragma solidity ^0.4.17;
contract FundRaise {
address public owner;
// @dev constructor function. Sets contract owner 
function FundRaise() {
owner = msg.sender;
}

// fallback function that allows contract to accept ETH
function () payable {}
}

And the result is:

Contract: FundRaise
✓ has an owner (38ms)
✓ is able to accept funds (234ms)
2 passing (473ms)

Beautiful. This is the process one goes through systematically to cover the smart contracts with test written in JavaScript. Just keep doing this process until all smart contract specifications are met.

For simplicity sake, I am going to fast forward the process and add the complete set of tests for the FundRaise contract so you have an idea how it would look like in the end.

Note the tests for the pause/unpause and owner receive funds tests

and here is the full code for the smart contract:

Run the tests one last time…

Contract: FundRaise
✓ has an owner (46ms)
✓ accepts funds (193ms)
✓ is able to pause and unpause fund activity (436ms)
✓ permits owner to remove funds (653ms)
4 passing (2s)

Marvelous!

Hope you learned a thing or two about testing smart contracts with JavaScript and its use in blockchain development. Now carry on and keep testing the heck out of your smart contracts.

The code for this blog post can be found here: https://github.com/gustavoguimaraes/smart-contract-testing-javascript-example-