Testing Your Smart Contract Behaviour With Truffle

in #utopian-io7 years ago (edited)

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:

  1. Allowing a punter to successfully place bet
  2. Successfully checks for the team that won the game
  3. 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

enter image description here

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

Sort:  

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

  • You have less than 500 followers. Just gave you a gift to help you succeed!
  • Seems like you contribute quite often. AMAZING!

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