Test your smart contract

To test our contract, we are going to use Hardhat Network, a local blockchain network designed for development which is similar to U2U network. It comes built-in with Hardhat, and it's used as the default network. You don't need to setup a U2U network to use it.

Now take a look at test folder in sample contract, it contains Lock.ts which is unit test file for contract Lock. Please delete that file contents and start to write tests from scratch.

Structure of a test file

Below is a basic structure of a test file:

describe("TestGroupName", function () {
  // Your test cases go here
  // it("A specific test case", function (){ /* ... */ })
  
  // Or subgroup(s) test
  // describe("SubgroupName1", function (){ /* ... */ })
});

The describe() function takes two arguments:

  • A string describing the group of tests (often referred to as the test suite's name or title), eg. "TestGroupName".

  • A callback function containing the test cases (it() statements) and potentially nested describe blocks.

describe() function is from Mocha testing framework. In Hardhat project, you don't need to import it explicitly. But if you prefer, you can still import explicitly by:

import { describe } from "mocha"

As long as the callback function does not throw any exceptions, the test is always passed. Let's run your test with Hardhat, it will produce the outcome as following:

Modify the test by writing your own checks (if-else statements) and use exceptions to verify your logic in tests:

Run it again:

However, by doing so is time-consuming and make your tests become messy. There is an test assertions framework called chai. Using it is easy, convert above checks to chai's test:

Run test again:

Now heading to create a test for contract.

Create a test

In our tests we're going to use ethers library from Hardhat (an extended library from ethers.js) to interact with Hardhat Network:

We also use time library from Hardhat toolbox:

And prepare some contants for contract deployment:

time.latest() returns the promise timestamp of latest block on the Hardhat Network.

Any calls to Hardhat network return promise. Please always remember to await a promise!

Then get the contract instance:

ethers.getContractFactory() accepts one argument, which is the name of the contract, ie. contract Lock {} in Lock.sol file.

By specifying wrong name, no contracts could be found.

Then call to deploy it:

The arguments of deploy include argument of the constructor of contract AND an override object.

Then finally our test:

Below is full test including contract deployment:

This is another test to ensure the owner field is set correctly:

ethers.getSigners() returns an array of 20 accounts with 10000 ETH each Hardhat Network for testing. Please check here for more.

By default, contracts are always deployed with the first account. Unless you specify it explicitly.

You have finished two tests, you see that the test setup (constants and deployment) are duplicated. Now let's try to use fixture!

Reusing common test setups with fixtures

The test setup could include numerous deployments and other transactions in more complicated projects. Duplicate code is created if that is done in each test. Additionally, doing a lot of transactions at the start of each test can make the test suite considerably slower.

Using fixtures will help you avoid duplicating code and will enhance the efficiency of your test suite. When a function is called for the first time, it only runs once and is known as a fixture. Hardhat will reset the state of the network to that of the time immediately following the fixture's initial execution on subsequent calls, as opposed to re-running it.

To use fixture, change your code as follow:

The fixture returns an object containing important data for testing purposes:

  • lock: The deployed instance of the "Lock" contract.

  • unlockTime: The calculated unlock time.

  • lockedAmount: The specified amount of coin locked in the contract during deployment.

  • owner: The account representing the contract owner.

  • otherAccount: The account representing another user.

Writing more tests

Should receive and store the funds to lock

lock.target returns the address where contract Lock is deployed on.

ethers.provider.getBalance() get balance of an address.

Should fail if the unlockTime is not in the future

expect().to.be.revertedWith(REVERT_ERROR_MESSAGE) is used check transaction revert.

Note that we don't await a reverted transaction, but instead we await the expect():

expect(await Lock.deploy(...))

await expect(Lock.deploy(...))

Shouldn't fail if the unlockTime has arrived and the owner calls it

expect().not.to.be.revertedWith(REVERT_ERROR_MESSAGE) is used check transaction success without revert.

Should emit an event on withdrawals

expect().to.emit() to assert an event is emitted.withArgs() is used to check arguments emitted with event.

anyValue is used when we don't want to check its explicitly. To import it: import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs";

Should transfer the funds to the owner

expect().to.changeEtherBalances() is used to check balance changes of accounts.

Full tests

You can find full unit test file as follow:

Last updated