Skip to content

Event‐driven Testing: Testing with Hardhat

Ananthan edited this page May 25, 2024 · 12 revisions

Hardhat comes with a built-in unit testing framework, which makes testing as easy as possible. The testing can be done in either JavaScript or TypeScript. Here, we will go with JavaScript. All the test files will be located inside the test directory.

First, initialize a Node.js project.

npm init

Install Hardhat.

npm i -D hardhat

Initialize Hardhat with an empty hardhat.config.js.

npx hardhat init

Install Hardhat toolbox and add it to hardhat.config.js.

npm i -D @nomicfoundation/hardhat-toolbox
require('@nomicfoundation/hardhat-toolbox')

The contract source code is given below. Copy and paste it to the 'contracts' folder inside the project.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Cert {
    address admin;
    event Issued(string indexed course, uint256 id, string grade);

    constructor() {
        admin = msg.sender;
    }

    modifier onlyAdmin() {
        require(msg.sender == admin, "Access Denied");
        _;
    }

    struct Certificate {
        string name;
        string course;
        string grade;
        string date;
    }

    mapping(uint256 => Certificate) public Certificates;

    function issue(
        uint256 _id,
        string memory _name,
        string memory _course,
        string memory _grade,
        string memory _date
    ) public onlyAdmin {
        Certificates[_id] = Certificate(_name, _course, _grade, _date);
        emit Issued(_course, _id, _grade);
    }
}

Writing Tests

For testing using JavaScript, Hardhat uses Mocha as the test runner and Chai for assertions; these provide a solid foundation upon which we can write our JavaScript tests.

Now each set of tests in Hardhat is declared using the describe() function, which breaks our test suite into components. Meanwhile it() is where we perform individual tests.

Hardhat leverages Ethers.js for communication with the simulated environment. Also, we will use the Hardhat plugin for fixtures to avoid code duplication and improve the performance of our test suite. A fixture is a setup function that is run only the first time it's invoked. On subsequent invocations, instead of re-running it, Hardhat will reset the state of the network to the point after the fixture was initially executed.

To know more about Mocha tests, please refer Mocha's documentation.

Now, let's start writing the test for our certificate application contract. The tests will check whether:

  • The contract is deployed correctly.
  • The authorized personnel can issue new certificates.
  • Anyone can retrieve the issued certificate details.
  • Only the authorized personnel is able to issue certificates.

First, create a new file named 'Cert.test.js' inside the 'test' directory. Let's start by importing the required modules and plugins.

const {
  loadFixture,
} = require('@nomicfoundation/hardhat-toolbox/network-helpers')
const { expect } = require('chai')

Let's define our describe() function.

describe('Cert', function () {})

Then, create an asynchronous function called deployCertFixture inside to initialize the fixture.

async function deployCertFixture() {
  const [admin, other] = await ethers.getSigners()
  const Cert = await ethers.getContractFactory('Cert')
  const cert = await Cert.deploy()
  return { cert, admin, other }
}

So, the function deployCertFixture will deploy our contract Cert inside the contracts folders using getContractFactory function from ethers (the function takes the contract name, not the file name). Then, in each test, we use loadFixture to run the fixture and get those values. loadFixture will run the setup the first time and quickly return to that state in the other tests. The function returns the deployed contract instance for interaction and two signers (admin, other) from the simulated test environment. We will use these signers to test the authorization of the smart contract soon.

Now, let us write our first test using it(). We are going to check whether the deployer is the first signer from the environment (admin).

it('Should set the right admin', async function () {
  const { cert, admin } = await loadFixture(deployCertFixture)
  expect(cert.deploymentTransaction().from).to.equal(admin.address)
})

Here, we expect the deployer address from the deployment to be equal to the address of the admin account.

Now, let's check whether the admin is able to issue a certificate to the contract. For that, we require events. Since we have defined an event Issued inside the contract, which emits the course, id and grade as its arguments. Since Hardhat supports event-driven testing, we can use emit, and withArgs functions for this utilization.

it('Should issue the certificate', async function () {
  const { cert } = await loadFixture(deployCertFixture)
  await expect(cert.issue(1024, 'Deren', 'CED', 'S', '24-04-2024'))
    .to.emit(cert, 'Issued')
    .withArgs('CED', 1024, 'S')
})

Now, let's check whether we can retrieve a certificate issued by the contract. Since we are using fixtures, the state will be switched to the initial one for each test; we first need to issue and then retrieve. Instead of using events, we use our public mapping Certificates in the smart contract to retrieve the data corresponding to the id. The Ethers.js library returns the result as an array. So, we need to check all arguments in their exact order as defined in the struct.

it('Should read the certificate', async function () {
  const { cert } = await loadFixture(deployCertFixture)
  await cert.issue(1024, 'Deren', 'CED', 'S', '24-04-2024')
  const certificate = await cert.Certificates(1024)

  expect(certificate[0]).to.equal('Deren')
  expect(certificate[1]).to.equal('CED')
  expect(certificate[2]).to.equal('S')
  expect(certificate[3]).to.equal('24-04-2024')
})

Lastly, we are going to ensure the issue function is only accessible by the deployer. Hence, we return the second signer from the fixture and use that account to issue a certificate. This can be achieved with the connect function in the contract instance cert. Since we are checking the modifier onlyAdmin we have to handle the revert operation initiated by the smart contract if require return false. For that, we can use revertedWith function from the Hardhat test suite.

it('Should revert the issuing', async function () {
  const { cert, other } = await loadFixture(deployCertFixture)
  await expect(
    cert.connect(other).issue(1024, 'Shalom', 'CBR', 'S', '23-03-2023')
  ).to.be.revertedWith('Access Denied')
})

The final test file is as follows:

const {
  loadFixture,
} = require('@nomicfoundation/hardhat-toolbox/network-helpers')
const { expect } = require('chai')

describe('Cert', function () {
  async function deployCertFixture() {
    const [admin, other] = await ethers.getSigners()

    const Cert = await ethers.getContractFactory('Cert')
    const cert = await Cert.deploy()

    return { cert, admin, other }
  }

  it('Should set the right admin', async function () {
    const { cert, admin } = await loadFixture(deployCertFixture)

    expect(cert.deploymentTransaction().from).to.equal(admin.address)
  })

  it('Should issue the certificate', async function () {
    const { cert } = await loadFixture(deployCertFixture)

    await expect(cert.issue(1024, 'Deren', 'CED', 'S', '24-04-2024'))
      .to.emit(cert, 'Issued')
      .withArgs('CED', 1024, 'S')
  })

  it('Should read the certificate', async function () {
    const { cert } = await loadFixture(deployCertFixture)

    await cert.issue(1024, 'Deren', 'CED', 'S', '24-04-2024')

    const certificate = await cert.Certificates(1024)

    expect(certificate[0]).to.equal('Deren')
    expect(certificate[1]).to.equal('CED')
    expect(certificate[2]).to.equal('S')
    expect(certificate[3]).to.equal('24-04-2024')
  })

  it('Should revert the issuing', async function () {
    const { cert, other } = await loadFixture(deployCertFixture)

    await expect(
      cert.connect(other).issue(1024, 'Shalom', 'CBR', 'S', '23-03-2023')
    ).to.be.revertedWith('Access Denied')
  })
})

For the full list of assert commands, please read Chai API docs. To execute the test, Use the following command in the root directory.

npx hardhat test

Our test conditions will be checked one by one, the final results will be logged onto the console.



Clone this wiki locally