In this tutorial, I will teach you how to perform error testing for your smart contract in Solidity.
Error testing is a crucial aspect of development especially when it comes to blockchain development. Unlike development using other technologies, blockchain technology charges a fee to modify data on the smart contract.
This is done to ensure security and complexity in data breach attempts by prowlers. Hence, it is better to test and handle errors in your smart contract so you don’t end up having your users pay for something they didn’t intend to do!
This guide aims to provide an example of how to perform error testing using Truffle and Mocha for your smart contracts in Solidity. So, let us get this guide started.
Getting Started with Smart Contract Error Testing in Solidity
Let us get started with learning to test errors for our smart contract. To begin, we will require a smart contract, which is what I will be creating now.
Creating A Sample Smart Contract
I am creating a lottery smart contract that has players and a manager in it. the person who starts the lottery becomes the manager and he/she has some functions entitled to him/her that only they can call.
Follow the below steps to create a contract:
- Create your project directory:
mkdir lotteryContract
- Create a new Node.js application:
npm init -y
- Open the project directory in your code editor
- Create a contracts directory from the terminal or your editor
- Create a file named ‘Lottery.sol’ inside the contracts directory
- Let us create a smart contract that allow players to enter into the contract. The manager can then pick a winner randomly. We will also have a random number generator function in our contract for this purpose.
After a winner is picked, the entire collected amount should be sent to the winner’s account. Let us see how we can write such a contract:
pragma solidity ^0.4.17;
contract Lottery {
address public manager;
address[] public players;
function Lottery() public {
manager = msg.sender;
}
function enter() public payable {
require(msg.value > .01 ether);
players.push(msg.sender);
}
function random() private view returns (uint256) {
return uint256(keccak256(block.difficulty, now, players));
}
function pickWinner() public {
require(manager == msg.sender);
uint256 index = random() % players.length;
players[index].transfer(this.balance);
players = new address[](0);
}
function getPlayers() public view returns (address[]) {
return players;
}
}
Explaining the code:
Our smart contract’s Solidity portion is now complete. It’s time for a quick code elaboration to help you understand what’s going on.
- The contract begins by declaring two variables: one is the address of the lottery’s manager, and the other is an array of lottery players’ addresses.
- We then immediately set the manager variable’s value to msg.sender, which indicates who created the contract (the manager).
- We then write an enter() function that anyone who wants to enter can use. Even the manager has the ability to enter. To enter and have his or her address added to the array of players, the participant must pay at least 0.01 ether.
- We then create the random() function, which, as you may have guessed, is a private function because we don’t want anyone outside the contract to call it. This random number generator function generates a random number in order to select a lottery winner.
It gives an unsigned integer value as a result. To generate a random integer, it uses the block difficulty score, the current time and date, and the players. A result is a random number.
- Next, we create a pickWinner() function that only the manager should use. We perform a quick authentication using the require() function once more. The random number is then used in a mod operation against the length of the player’s array.
- This outcome is saved in a variable called index. The index is now the index number of the winning player. We then immediately transfer the contract’s remaining balance to the winner. For this, we use the transfer(this.balance) function.
- We also instantly restore the state of our contract without having to redeploy it again and again by defining the length of the players array to 0.
- A getPlayers() function is also provided to retrieve the array of players.
Writing the Compile Script for the Contract
We now need a compiler in place for us that will compile our Solidity code and retrieve the ABI and bytecode of the contract. We can then take these and deploy our contract.
In your main project directory, create a file named compile.js:
const path = require("path");
const fs = require("fs");
const solc = require("solc");
const lotteryPath = path.resolve(__dirname, "contracts", "Lottery.sol");
const source = fs.readFileSync(lotteryPath, "utf8");
module.exports = solc.compile(source, 1).contracts[":Lottery"];
Perfect! We are now reading the contents of the Lottery.sol file using the built-in fs module and are exporting the extracted Lottery object from the contracts object.
Testing Solidity Smart Contract with Mocha
Let us now write some tests for our smart contract using Mocha.
- Create a directory named test
- Create a testing file name Lottery.test.js for Mocha testing
- In your package.lock, find the scripts key and set “test”: “mocha”
- Go to the test file and let us write some tests using Mocha:
const assert = require('assert');
const ganache = require('ganache-cli');
const { beforeEach } = require('mocha');
const Web3 = require('web3');
const web3 = new Web3(ganache.provider());
const { interface, bytecode } = require('../compile.js');
let lottery;
let accounts;
beforeEach(async () => {
accounts = await web3.eth.getAccounts()
lottery = await new web3.eth.Contract(JSON.parse(interface))
.deploy({ data: bytecode })
.send({ from: accounts[0], gas: '300000' });
});
describe('Lottery', () => {
it('deploys a contract', () => {
assert.ok(lottery.options.address);
});
it('allows one account to enter', async () => {
await lottery.methods.enter().send({
from: accounts[0],
value: web3.utils.toWei('.02', 'ether')
})
const players = await lottery.methods.getPlayers().call({
from: accounts[0]
})
assert.equal(accounts[0], players[0]);
assert.equal(1, players.length);
});
it('allows multiple account to enter', async () => {
await lottery.methods.enter().send({
from: accounts[0],
value: web3.utils.toWei('.02', 'ether')
});
await lottery.methods.enter().send({
from: accounts[1],
value: web3.utils.toWei('.02', 'ether')
});
await lottery.methods.enter().send({
from: accounts[2],
value: web3.utils.toWei('.02', 'ether')
});
const players = await lottery.methods.getPlayers().call({
from: accounts[0]
})
assert.equal(accounts[0], players[0]);
assert.equal(accounts[1], players[1]);
assert.equal(accounts[2], players[2]);
assert.equal(3, players.length);
});
it('requires a min ETH to enter', async () => {
try {
await lottery.methods.enter().send({
from: accounts[0],
value: 200
});
assert(false); // purposefully hrows an error no matter what
} catch (error) {
assert(error); //
}
});
it('checks if only manager can pick winner', async () => {
try {
await lottery.methods.pickWinner().send({
from: accounts[1]
})
assert(false);
} catch (error) {
assert(error);
}
});
it('sends money to winner and resets lottery', async () => {
await lottery.methods.enter().send({
from: accounts[0],
value: web3.utils.toWei('2', 'ether')
})
const initialBalance = await web3.eth.getBalance(accounts[0]);
await lottery.methods.pickWinner().send({
from: accounts[0]
});
const finalBalance = await web3.eth.getBalance(accounts[0]);
const difference = finalBalance - initialBalance;
console.log(difference);
assert(difference > web3.utils.toWei('1.8', 'ether'));
});
it('checks if players array is empty', async () => {
const players = await lottery.methods.getPlayers().call({
from: accounts[0]
})
assert(players.length == 0);
});
});
Let us now run our tests:
$ npm run test
> [email protected] test
> mocha
Lottery
√ deploys a contract
√ allows one account to enter (596ms)
√ allows multiple account to enter (1272ms)
√ requires a min ETH to enter (269ms)
√ checks if only manager can pick winner (253ms)
1999953164000002000
√ sends money to winner and resets lottery (797ms)
√ checks if players array is empty (159ms)
7 passing (7s)
Perfect! We have all 7 of our tests passing. This is a sign we can proceed for Truffle testing of our smart contract.
Testing Solidity Smart Contract with Truffle
Let us now write some tests for our smart contract using Truffle this time.
- Create a file named deploy.js
- Let us write our final test for our lottery contract:
const HDWalletProvider = require('@truffle/hdwallet-provider');
const Web3 = require('web3');
const { interface, bytecode } = require('./compile.js');
const provider = new HDWalletProvider(
'key shrimp shell dump about fog death unit hard find doomy surprise,
'https://rinkeby.infura.io/v3/900629bb1516987e9a08e762ef085a1a'
);
const web3 = new Web3(provider);
const deploy = async () => {
const accounts = await web3.eth.getAccounts();
console.log(`Attempting to deploy from account: ${accounts[0]}`);
const result = await new web3.eth.Contract(JSON.parse(interface))
.deploy({ data: bytecode })
.send({ gas: '1000000', from: accounts[0], gasLimit: '10000000' });
console.log(interface);
console.log('--------------------------------------------------------------------------------------------------------');
console.log(`The contract was successfully deployed to: ${result.options.address}`);
provider.engine.stop();
};
deploy();
Let us now run the deploy script and deploy the contract to the Rinkeby test net:
$ node deploy.js
Attempting to deploy from account: 0x5bA36E8dc1b0b5236f0a7330e0B34B70258F4De5
[{"constant":true,"inputs":[],"name":"manager","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"pickWinner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getPlayers","outputs":[{"name":"","type":"address[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"enter","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"players","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"}]
--------------------------------------------------------------------------------------------------------
The contract was successfully deployed to: 0xF6Acfcd0d7455836380C533CFcf7864dBaf2D0Fb
Amazing! We now have our contract deployed on the Rinkeby test network successfully!
Conclusion
Learn error testing of your smart contracts in Solidity using Truffle and Mocha.