Testing Your Smart Contract Behaviour With Truffle
Overview
This tutorial post is based on testing code for a smart contract for placing a bet that interacts with OraclizeAPI to get winner from a third party API. A post on how the smart contract was created and how it works can be found here.
We would be explaining in detail how to write test for the smart contract to check for the following behaviour:
- Allowing a punter to successfully place bet
- Successfully checks for the team that won the game
- Successfully distributes payouts to punters who placed bets on winning team
What Will I Learn
- How to test a smart contract using truffle
- Have your test run quickly while using your own personal ethereum blockchain
- Wait for your contract to fire an event before you run a test
- Write a less cluttered test using javascript async and await
Requirement
- A Linux machine
- Npm and Node installed
- Install truffle, run
npm install -g truffle
- Download & Install ganache here
- Clone the smart contract code, we are testing here.
- Clone & install ethereum-bridge, for running oraclize on testrpc. see how here
- Navigate to the project directory from your terminal and run
npm install
Difficulty
Intermediate
Setup
First few things that must be done to allow us test the smart contract include starting up ganache, which is quite easy if you have it installed already and also run ethereum bridge so our second and third test can run smoothly and lastly configure out truffle.js with the correct credentials.
To start ethereum bridge, navigate to the directory you cloned it on your terminal and run command node bridge -H localhost:7545 -a 0 --dev
.
You should be presented with a screen like this
Where it reads, Please add this line to your contract constructor
. Copy it, open the contract folder of the project and update the constructor of the Game.sol
file with OAR
you previously copied.
To configure or truffle.js
, open the settings in ganache and be sure the port no is same as the truffle.js
file. *
with work fine for the network id and 127.0.0.1
for the host.
Now we are ready to start writing our test
1: Allows A User To Successfully Place Bet
In this test we would assert if two people can successfully place a bet on a team each. In the test folder of the smart contract, create a folder tutorial and inside a file successfully_place_bet.js
.
Notice how we verbosely define the file name so another person at first glance knows what to expect from the test.
Taking a look a the constructor and bet method of the contract, we notice the contructor initializes with start and end time and the bet method have a modifier method that checks if the current time is less than start time used to initialize the contract.
//Constructor
function Game(uint start, uint end) public payable {
startTime = start;
endTime = end;
owner = msg.sender;
OAR = OraclizeAddrResolverI(0xF08dF3eFDD854FEDE77Ed3b2E515090EEe765154);
}
//Bet Method
function placeBet(string team) notStarted validContribution haveNoStake validTeam(team) public payable returns (uint) {
bettings.push(Punter({
account: msg.sender,
stake: msg.value,
supporting: team
}));
bettingAddresses[msg.sender] = bettings.length - 1;
Bet(msg.sender, msg.value);
}
So first we test to assert that the game has not started yet
const Contract = artifacts.require("./Game.sol");
//javascript library to help us convert
//time to timestamp to pass to the constructor
let moment = require("moment");
contract('Place Bet : ', async (accounts) => {
//Current timestamp
let now = moment().utc();
//Set start 10min from now & end time 1hr 40min from now
//considering a game is 90min
let start = now.add({minutes : 10}).unix();
let end = now.add({minutes: 100}).unix();
it('a user can only place bet only if game is not started', async () => {
let game = await Contract.new(start,end);
let status = await game.started();
assert.isFalse(status);
});
});
Notice the use of async
and await
onstead of the usuall javascript promise, makes things end up very neatly.
Run truffle test test/tutorial/a_user_can_place_bet.js
on your terminal.
The test would now pass with a message similar to
Contract: Place Bet :
✓ a user can only place bet only if game is not started (132ms)
Now we write a second test to check if the punter can place a valid contribution allowed by the bet method. A valid contribution is considered as a minimum of 0.1 eth
and a maximum of 1 eth
.
it('two users can stake between 0.01 and 1 ether on any game ones', async () => {
let game = await Contract.new(start,end);
await game.placeBet('realmadrid',{value: web3.toWei(0.6, "ether"), from: accounts[1]});
let betInfoA = await game.getAccountInfo(accounts[1]);
assert.equal(betInfoA[0], accounts[1]);
assert.equal(betInfoA[1], web3.toWei(0.6, "ether"));
await game.placeBet('swansea',{value: web3.toWei(0.4, "ether"), from: accounts[2]});
let betInfoB = await game.getAccountInfo(accounts[2]);
assert.equal(betInfoB[0], accounts[2]);
assert.equal(betInfoB[1], web3.toWei(0.4, "ether"));
})
If you open up your personal ethereum blockchain (ganache) the balances of the second and third account would have reduced by the amount used to place a bet.
Note: If balance is exhusted, just restart ganache and all accounts will be restored to 100eth each.
Run again truffle test test/tutorial/a_user_can_place_bet.js
The two test in the file will pass with a result similar to
Contract: Place Bet :
✓ a user can only place bet only if game is not started (132ms)
✓ two users can stake between 0.01 and 1 ether on any game ones (430ms)
2 passing (774ms)
Test: Successfully Checks For A Winner
In the tutorial directory of the test, create a file successfully_check_for_winner.js
. We begin by first testing if there is no winner for the game.
const Contract = artifacts.require('./Game.sol');
let moment = require("moment");
contract('Check For Winner : ', async (accounts) => {
//Current timestamp
let now = moment().utc();
//Set start 10min from now & end time 1hr 40min from now
//considering a game is 90min let start = now.add({minutes : 10}).unix();
let end = now.add({minutes: 100}).unix();
it('game has no winner', async () => {
let game = await Contract.new(start,end);
let winner = await game.winner();
assert.equal(winner,"");
});
});
Run this time on your terminal truffle test test/tutorial/successfully_check_for_winner.js
The test will successfully pass, now write the second test, to assert a game has a winner. This part of the second test is a little bit tricky as the contract will make a call to OraclizeAPI which communicate with a third party API to check if there is a winner. It means our test needs to wait for the request to be made successfully and return the result before we can assert if theres actually a winner.
A solution is to fire an event when oraclize returns a result and have your test wait and watch for the event then perform the assertion.
it('game has as a winner', async () => {
let game = await Contract.new(start,end);
await game.endGame();
await game.getWinner({value: web3.toWei(0.1, "ether")});
// Wait for the callback to be invoked by oraclize and the event to be emitted
const logWhenBetClosed = promisifyLogWatch(game.BetClosed({ fromBlock: 'latest' }));
let log = await logWhenBetClosed;
assert.equal(log.event, 'BetClosed', 'BetClosed not emitted');
assert.equal(log.args.result, 'swansea');
}).timeout(0);
/**
* @credit https://github.com/AdamJLemmon
* Helper to wait for log emission. * @param {Object} _event The event to wait for.
*/function promisifyLogWatch(_event) {
return new Promise((resolve, reject) => {
_event.watch((error, log) => {
_event.stopWatching();
if (error !== null)
reject(error);
resolve(log);
}); });}
Note: We set timeout to zero so mocha does not keep timing out before the result is returned.
Finally to our last testcase.
Test: Successfully Distributes Payouts To Punters
What the smart contract does eventually is check for the winner and takes the total sum of bet placed by those betting on the winning team to determine what ration of the total sum of the bet placed by the those betting on the lost team they get.
We can take this function to calculate what the result should be outside out test environment.
/**
* @param _bet amount bet by punter
* @param _totalPool total amount of winning side
* @param _totalPayable total amount of losing side
* @returns {number} amount payable to account calculated by ratio
*/let expectedPayable = (_bet,_totalPool,_totalPayable) => {
let percentage = (_bet/_totalPool) * 100;
let payble = percentage/100 * _totalPayable;
return payble * (10 ** 18);
};
let expectedPayout = expectedPayable(0.2,0.3,0.7);
Test full code
const Contract = artifacts.require("./Game.sol");
let moment = require("moment");
contract('Place Bet : ', async (accounts) => {
//Current timestamp
let now = moment().utc();
//Set start 10min from now & end time 1hr 40min from now
//considering a game is 90min let start = now.add({minutes : 10}).unix();
let end = now.add({minutes: 100}).unix();
it('successfully transfer payout to winners', async () => {
let game = await Contract.new(start,end);
//Place bets
await game.placeBet('swansea',{value: web3.toWei(0.1, "ether"), from: accounts[1]});
await game.placeBet('swansea',{value: web3.toWei(0.2, "ether"), from: accounts[2]});
await game.placeBet('realmadrid',{value: web3.toWei(0.3, "ether"), from: accounts[3]});
await game.placeBet('realmadrid',{value: web3.toWei(0.4, "ether"), from: accounts[4]});
//Account previous balance before placcing the bet
let prevAccBalance = await game.getBalance(accounts[2]);
//End game
await game.endGame();
//Retrieve winner from oracle
await game.getWinner({value: web3.toWei(0.4, "ether")});
// Wait for the callback to be invoked by oraclize and the event to be emitted
const logWhenBetClosed = promisifyLogWatch(game.BetClosed({ fromBlock: 'latest' }));
let log = await logWhenBetClosed;
assert.equal(log.event, 'BetClosed', 'BetClosed not emitted');
//Distribute payouts to accounts
//that bet on winner
await game.distributeStake();
//Convert from wei to ether
//since eth is 18 decimal places
let conversion = 10 ** 18;
let totalPayable = await game.totalPayable();
let accountPayable = await game.payouts(accounts[2]);
let newAccBalance = await game.getBalance(accounts[2]);
let totalPayableInEther = totalPayable.toNumber() / conversion;
let accountPayableInEther = accountPayable.toNumber() / conversion;
let prevAccBalanceInEther = prevAccBalance.toNumber() / conversion;
let newAccBalanceInEther = newAccBalance.toNumber() / conversion;
//assert.equal(totalPayableInEther, 0.3); //0.3 Sum of stake lost to swansea punters
assert.equal(prevAccBalanceInEther + accountPayableInEther, newAccBalanceInEther);
}).timeout(0);
/**
* @credit https://github.com/AdamJLemmon
* Helper to wait for log emission. * @param {Object} _event The event to wait for.
*/ function promisifyLogWatch(_event) {
return new Promise((resolve, reject) => {
_event.watch((error, log) => {
_event.stopWatching();
if (error !== null)
reject(error);
resolve(log);
});
});
}});
Now if you simply run truffle test
all our test would pass.
Note: If you run into issues while testing the contract, open an issue on the betting contract repository and i would be glad to help out.
Posted on Utopian.io - Rewarding Open Source Contributors
WARNING - The message you received from @altcoinalerts is a CONFIRMED SCAM!
DO NOT FOLLOW any instruction and DO NOT CLICK on any link in the comment!
For more information, read this post:
https://steemit.com/steemit/@arcange/virus-infection-threat-reported-searchingmagnified-dot-com
Please consider to upvote this warning or to vote for my witness if you find my work to protect you and the platform valuable. Your support is really appreciated!
Thanks for the contribution, it has been approved.
Need help? Write a ticket on https://support.utopian.io.
Chat with us on Discord.
[utopian-moderator]
Hey @alofe.oluwafemi I am @utopian-io. I have just upvoted you!
Achievements
Utopian Witness!
Participate on Discord. Lets GROW TOGETHER!
Up-vote this comment to grow my power and help Open Source contributions like this one. Want to chat? Join me on Discord https://discord.gg/Pc8HG9x
More Attention More Gain