Testing Solidity Smart Contracts

Testing Solidity Smart Contracts

In the previous blog, we discussed deploying our Smart Contract to the Rinkeby Test Network and interacting with it from our front end.

I decided to write this article on tests as a bonus. You should write these tests before writing the Smart Contract and integrating with the front end.

One way of testing our Contract is interacting with it manually and testing every scenario. In this, one needs to consider every scenario and test the Contract in that case. Remembering every edge case could be tedious. Also forgetting a single edge case could break our Contract.

This is not a preferred way of testing. Instead, we should write tests for our Smart Contracts. Writing tests will check our Smart Contract in every possible scenario and ensures our Contract behaves expectedly. If any of the test cases fail, we can fix that edge case during production only. Thus, writing tests for Smart Contracts is necessary.

So, let's write tests for our Lottery Contract.

Note: To Follow along, refer to this repo.

Navigate to the lottery-contract directory we created in the previous blog and create an empty directory named test. Inside the test directory, create an empty Lottery.test.js file.

For writing tests, we need to add a dependency to our project. For that run the following command in your root directory.

yarn add -D ganache-cli mocha

We have all pre-requisites for writing tests. Now, head on to the Lottery.test.js file and paste the following code.

const assert = require('assert');
const ganache = require('ganache-cli');
const Web3 = require('web3');

const web3 = new Web3(ganache.provider());
const { interface, bytecode } = require('../compile.js');

ganache-cli is a fast and customizable blockchain emulator that allows us to make calls to the blockchain without the overheads of running an actual Ethereum node. This will help us in performing our tests instantaneously.

For creating an instance of web3, we have used the provider from ganache as we will be deploying our Contract to the ganache local network for testing. Apart from web3, we have required assert (a native javascript library) and interface along with the bytecode from our compiled Contract.

Now, paste the following code.

let accounts;
let lottery;

beforeEach(async () => {
  accounts = await web3.eth.getAccounts();
  lottery = await new web3.eth.Contract(JSON.parse(interface))
    .deploy({ data: bytecode })
    .send({ from: accounts[0], gas: '1000000' });
});

We have declared two variables namely accounts and lottery that will store our accounts and lottery instance respectively.

After that, we have declared the beforeEach() function which will execute before every test case. In this function, we are fetching the accounts from the web3 instance and storing them in the accounts variable. After that, we have deployed the local instance of our Contract using web3 and stored it in the lottery variable.

Now, let's write our first test.

describe('Lottery Contract', () => {
  it('deploys a contract', () => {
    assert.ok(lottery.options.address);
  });
}

Above we have defined describe() function. It allows us to gather our tests into separate groupings within the same file, even multiple nested levels.

In the first argument of our describe() function, we have passed the name of our test-suite i.e. 'Lottery Contract'. Inside our describe() function, we have declared an it() function, inside which we have written our test. This test will ensure that our Contract gets deployed successfully.

The first argument of the it() function will accept the name of our test and the second argument will accept the function which runs our test. In this function, we have written assert.ok() which ensures that the value passed inside this function is not null.

We have written our first test. Now, let's run our test. For that navigate to the root directory and run the following command in the terminal.

yarn test

You should see the following output in your terminal.

image.png

The tick in front of the test name indicates that our test has successfully passed. Congratulations! You have written your first test.

Now let's write tests for other scenarios as well.

For that paste the code from below inside the describe() function.

  it('allows 1 account to enter', async () => {
    await lottery.methods.enter().send({
      from: accounts[0],
      value: web3.utils.toWei('0.02', 'ether'),
    });

    const players = await lottery.methods.getPlayers().call({
      from: accounts[0],
    });

    assert.strictEqual(accounts[0], players[0]);
    assert.strictEqual(1, players.length);
  });

This test will check whether our Lottery allows users to enter the Lottery. For that, we are initially entering the lottery by calling the lottery.methods.enter() method. Following that, we are fetching the players of the lottery by calling the lottery.methods.getPlayers() method. Our players variable will be an array containing the addresses of all the players of the contract.

Now, we have called the assert.strictEqual() method which ensures that both the arguments passed to it are strictly equal. This test will ensure that we can enter our Lottery successfully.

Similarly, we'll check for multiple accounts to enter our lottery. For that paste the code from below.

   it('allows multiple accounts to enter', async () => {
    await lottery.methods.enter().send({
      from: accounts[0],
      value: web3.utils.toWei('0.02', 'ether'),
    });

    await lottery.methods.enter().send({
      from: accounts[1],
      value: web3.utils.toWei('0.02', 'ether'),
    });

    await lottery.methods.enter().send({
      from: accounts[2],
      value: web3.utils.toWei('0.02', 'ether'),
    });

    const players = await lottery.methods.getPlayers().call({
      from: accounts[0],
    });

    assert.strictEqual(accounts[0], players[0]);
    assert.strictEqual(accounts[1], players[1]);
    assert.strictEqual(accounts[2], players[2]);
    assert.strictEqual(3, players.length);
  });

In this test, we are entering the lottery from multiple accounts and after that, we are ensuring that each player can enter the lottery or not by calling the assert.strictEqual() method.

After this, we'll write a test to ensure that users can't enter with ethers less than the required amount to enter the lottery. For that, paste the code below.

  it('requires minimum amount of ether to enter', async () => {
    try {
      await lottery.methods.enter().send({
        from: accounts[0],
        value: 0,
      });
      assert(false);
    } catch (err) {
      assert(err);
    }
  });

This test will make sure that the test fails when a user tries to enter with ethers less than the required amount and pass the test when the user is unable to enter the lottery.

Following this, we will write a test that tests that only the manager could able to pick a winner. For that, paste the below code.

it('only manager can pick winner', async () => {
    try {
      await lottery.methods.pickWinner().send({
        from: accounts[1],
      });
      assert(false);
    } catch (err) {
      assert(err);
    }
  });

Remember in our beforeEach() function, we have deployed the contract using accounts[0]. Thus, the address of our manager is the address stored at accounts[0]. As a result, our test should fail if we try to pick a winner from an account other than accounts[0].

This test will ensure that only our manager is allowed to pick a winner.

At last comes our final test, which ensures that ethers are sent to the winner of the Contract.

For that, paste the below test.

it('sends money to the winner and resets the players array', 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: ', difference);
    assert(difference > web3.utils.toWei('1.8', 'ether'));

    const players = await lottery.methods.getPlayers().call({ from: accounts[0]})

    assert.strictEqual(0, players.length)
  });

This test ensures that lottery ethers are sent to the winner of the lottery on picking up the winner by the manager of the contract.

We have successfully written all the necessary tests. Now let's run these tests. For that, navigate to the root directory and run the yarn test command.

You should see the following output in the terminal.

image.png

Congratulations! You have successfully written tests for your Smart Contract. These tests provide security that your Contract won't break out. Now we can be more reliable on your Smart Contracts and can be 100% sure that our Smart Contract will not misbehave.

Connect with me on twitter.