Unit Testing a Solidity Smart Contract using Chai & Mocha with TypeScript
Tags: blockchain, typescript, webdev, testing
Published: 2021-10-21 01:32:17

## Overview

I've recently been playing around writing some smart contracts for fun with Solidity and the fastest way to validate the logic you wrote works as what you expect it to do is by unit testing. It was also a fun experiencing testing smart contracts with Chai and Mocha together with TypeScript. It made things quick and easy for me. And cheers to Hardhat for making things much more easier and convenient that it automatically generates all the TypeScript typings of a smart contract via Typechain. In this article we're only going to cover how we set it up using Hardhat and how we can make those assertions. 

But why should we unit test our smart contracts? Can't we just deploy it manually using the Hardhat deploy script and from the UI we can point-click and test it from there? Yes, we can but eventually it's going to take us so much time, you can count off how many steps it took to validate that the smart contract that we wrote has been working as what we expect. With unit testing, we can directly make calls to those methods and make the assertions or even console logging if you so desire to in a single test file.

However, if you're still new to Ethereum Development or Blockchain Development, then be sure to check out [Nader Dabit's article for a complete guide to Full Stack Ethereum Development](https://dev.to/dabit3/the-complete-guide-to-full-stack-ethereum-development-3j13) 🔥

With all that being said let's get right on to it. 🚢

## The Stack

Outlining the stack that we are using for this tutorial:

- [Solidity](https://soliditylang.org/)
- [Hardhat](https://hardhat.org/)
- [Chai](https://www.chaijs.com/)
- [Mocha](https://mochajs.org/)
- [TypeScript](https://www.typescriptlang.org/)

## Installation

As mentioned above we are going to use Hardhat for this. Just in case you don't know what Hardhat is, it provides a development environment for Ethereum based projects. It's very intuitive I like it and the documentation is also great. You can visit the docs from [here](https://hardhat.org/).

Let's get started.

Setup a new directory called `smart-contract-chai-testing` (Or whichever you prefer to name it)

```bash
# you can omit the $ sign
$ mkdir smart-contract-chai-testing
```

Then navigate into the new directory created

```bash
$ cd smart-contract-chai-testing
```

We'll initialize a local Git repository to make things easier for us to visually see in the source control on what things were recently added or modified (I prefer it this way but you can omit this step)

```bash
$ git init
```

Next is we'll initialize a Hardhat project with the following command (Assuming that you have Node.js installed already in your machine)

```bash
$ npx hardhat init
```

You should then see the following output in your terminal

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zbe9kwjfoui9eictit9e.png) 

And proceed to selecting **"Create an advanced sample project that uses TypeScript"**, it will just scaffold everything for us. When that gets selected, just say yes (Y) to all questions the CLI asks.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/33jcf8kdex57l3owemq7.png) 

Now that's all setup we can open the code in our favorite IDE, the almighty Visual Studio Code.

```bash
$ code .
```

And finally stage all the changes and commit with "Init" message

```bash
$ git add . && git commit -m "Init"
```

## Running a Test

As you notice like any other Hardhat initial projects, we got a `Greeter` smart contract. In this scaffolding we also have that and got a test case on TypeScript. So to trigger a test execute the following command in your terminal. A test on `test/index.ts` will get executed.

```bash
$ npx hardhat test 
```

When that is called it is going to compile your smart contract as quickly as possible and with Hardhat's Typechain it's going to auto-generate all the TypeScript typings for that smart contract `Greeter`. That's very convenient isn't it? When writing smart contracts with Hardhat's Typechain plugin or library, there's literally zero boilerplate.

You can see it for yourself and inspect the directory generated called `typechain` along with the `artifcats` directory that was generated too as the smart contract was compiled.

## Updating the Greeter smart contract

Ok so we'll modify our `Greeter` contract to have some slightly interesting test cases. In this smart contract it should do the following:

- Should have a function `sum` that returns the sum of two numbers provided
- A user can store their lucky number, they are only allowed to store a lucky number when they don't have a lucky number stored yet. Otherwise the execution will revert.
- A user can update their lucky number, only if they remember correctly their previous lucky number. Otherwise the execution will revert.

The requirements are a bit confusing aren't they? But that's exactly why we should be writing tests so we exactly know its behavior on-chain and we can fix up bugs during development before it gets finally deployed to Mainnet.

You can update your `Greeter` smart contract and we'll then create test cases for it.

```sol
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.4;

import "hardhat/console.sol";

contract Greeter {
    mapping(address => uint256) public ownerToLuckyNumber;

    constructor() {
        console.log("Deployed Greeter by '%s'", msg.sender);
    }

    function sum(uint256 a, uint256 b) public pure returns (uint256) {
        return a + b;
    }

    function getMyLuckyNumber() external view returns (uint256) {
        return ownerToLuckyNumber[msg.sender];
    }

    modifier luckyNumberGuard() {
        /// @dev if it's not 0 then owner already has a lucky number
        require(
            ownerToLuckyNumber[msg.sender] == 0,
            "You already have a lucky number."
        );
        _;
    }

    modifier luckyNumberNotZero(uint256 _luckyNumber) {
        require(_luckyNumber != 0, "Lucky number should not be 0.");
        _;
    }

    function saveLuckyNumber(uint256 _luckyNumber)
        external
        luckyNumberGuard
        luckyNumberNotZero(_luckyNumber)
    {
        ownerToLuckyNumber[msg.sender] = _luckyNumber;
    }

    modifier shouldMatchPreviousLuckyNumber(uint256 _luckyNumber) {
        require(
            ownerToLuckyNumber[msg.sender] == _luckyNumber,
            "Not your previous lucky number."
        );
        _;
    }

    function updateLuckyNumber(uint256 _luckyNumber, uint256 _newLuckyNumber)
        external
        shouldMatchPreviousLuckyNumber(_luckyNumber)
    {
        ownerToLuckyNumber[msg.sender] = _newLuckyNumber;
    }
}
```

I've written few functions and few modifiers just to make things a bit interesting to test.

## Writing our tests

Now we got our smart contract up to date to the requirements, the fun starts and we'll start testing it. Also I like to group my tests by function names on those function I am going to test it, it makes things easier to navigate in this test suite when things get bigger. Not only that but it helps me navigate easily when after a few months I have to look back at it again.

So basically I have the following structure:

```jsx
describe("Greeting", () => {
  describe("functionName", () => {
		it("should return when given", async () => {
			// ...
    })
  })
})
```

The first `describe` is the smart contract name and each child `describe`s are all the functions that are in that smart contract. But you can structure your tests in any way shape or form that you think it's much easier to go with. 

### Writing a test for function sum

Here's a test case for testing the `sum` function

```ts
import { expect } from "chai";
import { ethers } from "hardhat";
import { Greeter } from "../typechain";

describe("Greeter", function () {
  let contract: Greeter;

  beforeEach(async () => {
    const Greeter = await ethers.getContractFactory("Greeter");
    contract = await Greeter.deploy();
  });

  describe("sum", () => {
    it("should return 5 when given parameters are 2 and 3", async function () {
      await contract.deployed();

      const sum = await contract.sum(2, 3);

      expect(sum).to.be.not.undefined;
      expect(sum).to.be.not.null;
      expect(sum).to.be.not.NaN;
      expect(sum).to.equal(5);
    });
  });
});
```

For a quick breakdown, I created a local variable and used `let` to reassign it but have a type of `Greeter` which was generated by Typechain as the smart contract was compiled. And on the `beforeEach` for every test case it's going to execute that and set the value for the `contract` variable. So basically in all test cases we can just directly grab it off from there and not having to copy-paste the contract in each single test cases.

Now run the tests by the following command

```bash
$ npx hardhat test
```

You might want to make the script a bit shorter if you're using `yarn` or `npm` so open up your `package.json` file and add a script for it.

```json
// package.json
{
	// ...	

	"scripts": {
	  "test": "hardhat test"
	},

	// ...
}
```

And now we can run the tests by `yarn test` or `npm test` then we get the following result

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m13w3tprwi45zy8ssyaa.png) 

### Writing a test for getMyLuckyNumber function

Before we can assert `getMyLuckyNumber` we'll first have to save our lucky number into the smart contract to set the state in `ownerToLuckyNumber` and grab the value from there. For a short breakdown on what's happening on the test below. We have the contract deployed and we called `saveLuckyNumber` and pass down a value of `5` and once that's done we call `getMyLuckyNumber` and store it to a local variable `myLuckyNumber` we then proceed to assert and expect it to be not `undefined` and to be not `null` and finally since `uint256` is considered a `BigNumberish` type we just then convert it to a simple JavaScript friendly number by calling `toNumber` on object type `BigNumberish` which is our `myLuckyNumber` local variable. Then just expect it to equal to `5` because we previously called `saveLuckyNumber` with value of `5`. 

```ts
import { expect } from "chai";
import { ethers } from "hardhat";
import { Greeter } from "../typechain";

describe("Greeter", function () {
  let contract: Greeter;

  beforeEach(async () => {
    const Greeter = await ethers.getContractFactory("Greeter");
    contract = await Greeter.deploy();
  });

  // ...

  describe("getMyLuckyNumber", () => {
    it("should return 5 when given 5", async () => {
      await contract.deployed();

      await contract.saveLuckyNumber(5);
      const myLuckyNumber = await contract.getMyLuckyNumber();

      expect(myLuckyNumber).to.be.not.undefined;
      expect(myLuckyNumber).to.be.not.null;
      expect(myLuckyNumber.toNumber()).to.equal(5);
    });
  });

  // ...
});
```

Now run that test by `yarn test` or `npm test` then we get the results

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jvso9q3561w5z1kq4wxe.png)

### Writing tests for saveLuckyNumber function

Now here we can test out and expect reverts when a condition in the modifier does not satisfy.

```ts
import { expect } from "chai";
import { ethers } from "hardhat";
import { Greeter } from "../typechain";

describe("Greeter", function () {
  let contract: Greeter;

  beforeEach(async () => {
    const Greeter = await ethers.getContractFactory("Greeter");
    contract = await Greeter.deploy();
  });

  // ...

  describe("saveLuckyNumber", () => {
    it("should revert with message 'Lucky number should not be 0.', when given 0", async () => {
      await contract.deployed();

      await expect(contract.saveLuckyNumber(0)).to.be.revertedWith(
        "Lucky number should not be 0."
      );
    });

    it("should revert with message 'You already have a lucky number.', when owner already have saved a lucky number", async () => {
      await contract.deployed();

      await contract.saveLuckyNumber(6);

      await expect(contract.saveLuckyNumber(7)).to.be.revertedWith(
        "You already have a lucky number."
      );
    });

    it("should retrieve 66 when recently given lucky number is 66", async () => {
      await contract.deployed();

      await contract.saveLuckyNumber(66);
      const storedLuckyNumber = await contract.getMyLuckyNumber();

      expect(storedLuckyNumber).to.be.not.undefined;
      expect(storedLuckyNumber).to.be.not.null;
      expect(storedLuckyNumber).to.be.not.equal(0);
      expect(storedLuckyNumber).to.be.equal(66);
    });
  });

  // ...
}); 
```

For the first 2 tests, we intently make it fail by going against the modifiers that we defined. The modifiers are there to protect anything from unexpected behaviors, thus reverts it when it fails to satisfy the modifier. 

For a breakdown of each test cases on `saveLuckyNumber`:

- On the first test case, we pass down a value of `0` into `saveLuckyNumber` and on the smart contract on `saveLuckyNumber` we have a modifier attached to the function prototype. It expects a lucky number value anything other than `0`. So when it's `0` it will always fail and will revert. Thus we have our expect assertion that the call will be reverted with the following message `"Lucky number should not be 0."`.
- On the second test case, on the `saveLuckyNumber` function again we have a modifier defined that they will never be able to set a new lucky number from calling `saveLuckyNumber` because it's going to check into the mapping `ownerToLuckyNumber` to see if they have already a lucky number stored. If they do we'll never allow it to update the state in `ownerToLuckyNumber` of their address. Thus reverting with message `"You already have a lucky number."`
- On the third test case, we just simply call and pass down a value of `66` to `saveLuckyNumber` and eventually just call `getMyLuckyNumber` and expect it to return `66`

Now we'll run the tests again by `yarn test` or `npm test` and we have the following result

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4jhrrz29ltoug5ls2ndm.png) 
 
### Writing tests for updateLuckyNumber

Finally for the last function that we will be testing. This function allows the User to update their existing lucky number on-chain only if so they remember their previous lucky number. If they don't remember then they can just check for it by calling `getMyLuckyNumber`

```ts
import { expect } from "chai";
import { ethers } from "hardhat";
import { Greeter } from "../typechain";

describe("Greeter", function () {
  let contract: Greeter;

  beforeEach(async () => {
    const Greeter = await ethers.getContractFactory("Greeter");
    contract = await Greeter.deploy();
  });

  // ...

  describe("updateLuckyNumber", () => {
    it("should revert with message '', when the given lucky number does not match with their existing lucky number", async () => {
      await contract.deployed();
      await contract.saveLuckyNumber(6);

      await expect(contract.updateLuckyNumber(8, 99)).to.be.revertedWith(
        "Not your previous lucky number."
      );
    });

    it("should update their lucky number, when given the exact existing lucky number stored", async () => {
      await contract.deployed();
      await contract.saveLuckyNumber(2);

      await contract.updateLuckyNumber(2, 22);
      const newLuckyNumber = await contract.getMyLuckyNumber();

      expect(newLuckyNumber).to.be.not.undefined;
      expect(newLuckyNumber).to.be.not.null;
      expect(newLuckyNumber.toNumber()).to.be.equal(22);
    });
  });
});
```

To break it down for you:

- On the first test, we intently make the test fail. Say for a given scenario the User forgets their previous lucky number saved on-chain. So it shouldn't allow them to update their lucky number. So in the test we'll make them save lucky number of `6` and then some time around in the future they want to update it. So we call `updateLuckyNumber` where we pass value of `8` as the first function argument which is their "previous" lucky number because they believe so it was `8` and `99` as the second function argument which is the new lucky number they want to replace. So the smart contract will most likely prevent updating the state or their lucky number stored on-chain. Now we can safely assert that it was reverted with the message `"Not your previous lucky number."` (For now it's that, error message not too detailed but we're not concerned about good design in the scope of this tutorial)
- On the second test, say User actually remembered their previous lucky number and they want to update it to a new lucky number. Then we assert the new lucky number which has a value of `22`

Finally we'll run the tests one more time

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/phaj9nti1gvurx026h6f.png) 

And every thing passed! That gave us the assurance that our smart contract works as what we expect it to do. So that's a wrap!

## Conclusion

We've covered creating a new project and a setup for Hardhat using the template that they provided which was extremely cool where we don't have to worry as much setting up those configurations ourselves. We've also learned how to make assertions in Chai and to expect certain values and expect a reversion with a revert message. And finally we notice how Typechain is working so well, it does most of the work for us by generating those types automatically every time we compile the smart contract, that was very convenient! 

That's it for me, I hope you enjoy reading and hope that you learned something. Thanks for reading up until this point, have a good day and cheers!

If you haven't already joined a DAO, join your first DAO by going here [@developer_dao](https://twitter.com/developer_dao) go and mint your DAO NFT, get a pixel avatar and vibe into the Discord channel. See you there!

If you might have any questions or suggestions feel free to drop comments below I'd be happy!

Full source code available in the [repository](https://github.com/carlomigueldy/tut-solidity-chai-testing)