/ ethereum / 5 min read

Checking events when testing Solidity smart contracts with Truffle

When testing Solidity smart contracts it is very helpful to check the events generated by those contracts. Especially asserting the arguments of the emitted events is a powerful tool. However, using web3 to watch for events inside tests proved to be impractical, and manually going through the returned transaction receipt can be tedious. This is why we will discuss using truffle-assertions in order to make straightforward assertions about smart contract events and their arguments.

I created the truffle-assertions library to assert that certain event types were emitted during a transaction, and especially to add complex conditions to the event arguments. In this guide we will explore how to use this library in order to check that certain events were emitted by a test contract and that their arguments fulfill the required conditions.

Prerequisites

Before we start, we need Truffle, which can be installed using npm.

npm install -g truffle

We also need some sort of Ethereum test network, such as Ganache, which can be installed from their website, or with Homebrew if you are using macOS.

brew cask install ganache

Next we create a new project directory, and initialise a new Truffle project in it.

mkdir truffle-event-tests
cd truffle-event-tests
truffle init
npm init -y

Finally, the truffle.js configuration file should point to the correct test network.

module.exports = {
  networks: {
    development: {
      host: "127.0.0.1",
      port: 7545,
      network_id: "*"
    }
  }
};

The contract

For this guide we will use a simple number betting contract. With this contract a user can bet ether on a number from 1 to 10, getting paid back ten times their initial bet if they pick the correct number. We will also add the possibility to fund the contract. Finally, we will make sure that a player can only bet a small percentage of the contract's balance, so players can not bankrupt the contract by winning too often. The winning number is generated as a modulo operation on the current block, which is fine for our testing purpose, but note that it is easily abused if it would be published.

contracts/Casino.sol

pragma solidity 0.4.23;

contract Casino {
    address public owner;

    event PlayEvent(address player, uint256 betSize, uint8 betNumber, uint8 winningNumber);
    event PayoutEvent(address winner, uint256 payout);

    constructor() public {
        owner = msg.sender;
    }

    function kill() external {
        require(msg.sender == owner, "Only the owner can kill this contract");
        selfdestruct(owner);
    }

    function fund() external payable {}

    function bet(uint8 number) external payable {
        require(msg.value <= getMaxBet(), "Bet amount can not exceed max bet size");
        require(msg.value > 0, "A bet should be placed");

        uint8 winningNumber = generateWinningNumber();
        emit PlayEvent(msg.sender, msg.value, number, winningNumber);

        if (number == winningNumber) {
            payout(msg.sender, msg.value * 10);
        }
    }

    function getMaxBet() public view returns (uint256) {
        return address(this).balance / 100;
    }

    function generateWinningNumber() internal view returns (uint8) {
        return uint8(block.number % 10 + 1); // Don't do this in production
    }

    function payout(address winner, uint256 amount) internal {
        assert(amount > 0);
        assert(amount <= address(this).balance);

        winner.transfer(amount);
        emit PayoutEvent(winner, amount);
    }
}

Testing this contract

To test this contract's functionality, we will verify that PlayEvents are emitted by the contract, and that its arguments match our expectations. We will also verify that a PayoutEvent is only emitted when the player bets on the correct number, and that their payout is the correct amount.

Install the testing packages

First we will install the Chai assertions library (or a different assertions library) for generic assertions, and the truffle-assertions library for the event specific assertions.

npm install --save-dev chai truffle-assertions

Write the tests

With the dependencies satisfied, they can be imported at the top of the test, and the correct functions can be used inside the tests. The contract will also be imported at this point.

const Casino = artifacts.require('Casino');
const assert = require("chai").assert;
const truffleAssert = require('truffle-assertions');

After this, we define a new contract scope, and add beforeEach and afterEach hooks to create a new contract for every test and to tear it down again.

contract('casino', async (accounts) => {
    let casino;
    const fundingAccount = accounts[0];
    const bettingAccount = accounts[1];
    const fundingSize = 100;

    // build up and tear down a new Casino contract before each test
    beforeEach(async () => {
        casino = await Casino.new({from: fundingAccount});
        await casino.fund({from: fundingAccount, value: fundingSize});
        assert.equal(web3.eth.getBalance(casino.address).toNumber(), fundingSize);
    });
    
    afterEach(async () => {
        await casino.kill({from: fundingAccount});
    });
}

Since we understand how the algorithm determines the winning number, we know beforehand whether a bet will be winning or losing. So we can define tests for each case.

We will start by defining a test for the losing case. In this test we want to intentionally bet on the wrong number, after which we want to assert that a PlayEvent has been emitted with the correct arguments, but that no PayoutEvent has been emitted, because the player lost the bet. We do this by using the assertEventEmitted, and assertEventNotEmitted functions from the truffle-assertions library. Both take a transaction receipt to look in, the expected event type, and an optional filter function with additional conditions to the event's arguments.

it("should lose when bet on the wrong number", async () => {
    // given
    let betSize = 1;
    // we know what the winning number will be since we know the algorithm
    let betNumber = web3.eth.getBlock("latest").number % 10 + 1;

    // when
    let tx = await casino.bet(betNumber, {from: bettingAccount, value: betSize});

    // then
    // player should be the same as the betting account, and the betted number should not equal the winning number
    truffleAssert.eventEmitted(tx, 'PlayEvent', (ev) => {
        return ev.player === bettingAccount && !ev.betNumber.eq(ev.winningNumber);
    });
    // there should be no payouts
    truffleAssert.eventNotEmitted(tx, 'PayoutEvent');
    // check the contract's balance
    assert.equal(web3.eth.getBalance(casino.address).toNumber(), fundingSize + betSize);
});

Finally, we add a test for the winning case, where we want to assert that both a PlayEvent and a PayoutEvent have been emitted with the correct arguments.

it("should win when bet on the right number", async () => {
    // given
    let betSize = 1;
    // we know what the winning number will be since we know the algorithm
    let betNumber = (web3.eth.getBlock("latest").number + 1) % 10 + 1;

    // when
    let tx = await casino.bet(betNumber, {from: bettingAccount, value: betSize});

    // then
    // player should be the same as the betting account, and the betted number should equal the winning number
    truffleAssert.eventEmitted(tx, 'PlayEvent', (ev) => {
        return ev.player === bettingAccount && ev.betNumber.eq(ev.winningNumber);
    });
    // player should be the same as the betting account, and the payout should be 10 times the bet size
    truffleAssert.eventEmitted(tx, 'PayoutEvent', (ev) => {
        return ev.winner === bettingAccount && ev.payout.toNumber() === 10 * betSize;
    });
    // check the contract's balance
    assert.equal(web3.eth.getBalance(casino.address).toNumber(), fundingSize + betSize - betSize * 10);
});

When we put everything together we end up with the following smart contract test:

test/testcasino.js

const Casino = artifacts.require('Casino');
const assert = require("chai").assert;
const truffleAssert = require('truffle-assertions');

contract('casino', async (accounts) => {
    let casino;
    const fundingAccount = accounts[0];
    const bettingAccount = accounts[1];
    const fundingSize = 100;

    // build up and tear down a new Casino contract before each test
    beforeEach(async () => {
        casino = await Casino.new({from: fundingAccount});
        await casino.fund({from: fundingAccount, value: fundingSize});
        assert.equal(web3.eth.getBalance(casino.address).toNumber(), fundingSize);
    });
    
    afterEach(async () => {
        await casino.kill({from: fundingAccount});
    });

    it("should lose when bet on the wrong number", async () => {
        // given
        let betSize = 1;
        // we know what the winning number will be since we know the algorithm
        let betNumber = web3.eth.getBlock("latest").number % 10 + 1;

        // when
        let tx = await casino.bet(betNumber, {from: bettingAccount, value: betSize});

        // then
        // player should be the same as the betting account, and the betted number should not equal the winning number
        truffleAssert.eventEmitted(tx, 'PlayEvent', (ev) => {
            return ev.player === bettingAccount && !ev.betNumber.eq(ev.winningNumber);
        });
        // there should be no payouts
        truffleAssert.eventNotEmitted(tx, 'PayoutEvent');
        // check the contract's balance
        assert.equal(web3.eth.getBalance(casino.address).toNumber(), fundingSize + betSize);
    });

    it("should win when bet on the right number", async () => {
        // given
        let betSize = 1;
        // we know what the winning number will be since we know the algorithm
        let betNumber = (web3.eth.getBlock("latest").number + 1) % 10 + 1;

        // when
        let tx = await casino.bet(betNumber, {from: bettingAccount, value: betSize});

        // then
        // player should be the same as the betting account, and the betted number should equal the winning number
        truffleAssert.eventEmitted(tx, 'PlayEvent', (ev) => {
            return ev.player === bettingAccount && ev.betNumber.eq(ev.winningNumber);
        });
        // player should be the same as the betting account, and the payout should be 10 times the bet size
        truffleAssert.eventEmitted(tx, 'PayoutEvent', (ev) => {
            return ev.winner === bettingAccount && ev.payout.toNumber() === 10 * betSize;
        });
        // check the contract's balance
        assert.equal(web3.eth.getBalance(casino.address).toNumber(), fundingSize + betSize - betSize * 10);
    });
});

Running the tests

After writing the contract and the test, we can verify that it is actually working, by running all truffle tests.

truffle test

Which should result in an output similar to this:

Using network 'development'.

Compiling ./contracts/Casino.sol...


  Contract: casino
    ✓ should lose when bet on the wrong number (372ms)
    ✓ should win when bet on the right number (490ms)


  6 passing (5s)

Conclusion

Events are already powerful tools within the smart contract development tool chain, and the truffle-assertions library allows you to use these events to test your smart contracts in a very straightforward way. The library offers a way to add any conditions for the event arguments to these assertions using a filter function, making it easy to look for very specific events.


If you found this guide useful and wish to use truffle-assertions for your own use case, check it out on npm or github. If you used this library in testing your own Ethereum smart contracts or if you just feel like sharing, tell me about your Dapps in the comments below. And don't forget to share this with your smart contract developer friends on Facebook, Twitter and LinkedIn.