Skip to main content

๐Ÿงช Tests

Introductionโ€‹

In this section, we focus on the standards for writing tests. Following these guidelines ensures our tests are both robust and intuitively structured for ease of use and future modifications.

Guidelinesโ€‹

1 Formattingโ€‹

Tests and helper scripts are written in TypeScript. We follow Google's TypeScript Style Guide for fundamental coding principles. To ensure consistency, we use a Prettier Plugin for formatting. Like Solidity, you can format TypeScript code manually like this.

$ npm run lint-ts-fix

This command is automatically executed before each commit via the Husky pre-commit hook, so you don't have to worry about running it yourself.

2 Coverageโ€‹

We aim to maintain 100% all files test coverage. For this, we rely on the solidity-coverage package. To check the coverage, execute the following command.

$ npm run coverage

Expected output.

File% Stmts% Branch% Funcs% Lines
All files100100100100

Sometimes, reaching 100% coverage can be challenging. For these situations, we recommend creating a contract within the contracts/mock folder. While mock contracts themselves are not included in the coverage metrics, they are highly beneficial. These can either inherit from the contract you're testing or provide specific functions to test a library. In tests, you can use these mock contracts to replace the original contract or library.

contract PriorityQueueMock {
using PriorityQueue for *;

PriorityQueue.UintQueue internal _uintQueue;

function addUint(uint256 value_, uint256 priority_) external {
_uintQueue.add(value_, priority_);
}
}

Additionally, make use of the helpful tools available in the @nomicfoundation/hardhat-network-helpers package, which is already installed in our repo. These tools can also assist in setting up test scenarios that might be difficult to replicate, such as time-based tests.

import { time } from "@nomicfoundation/hardhat-network-helpers";

await time.increase(60);

3 Code organizationโ€‹

Tests are written using the Mocha framework. Our main rule for test organization is to create one test file for each contract, and use one describe block for each contract function. Furthermore, the title of each it block has to begin with should... to clearly articulate the expected behavior or outcome.

Suppose we have two contracts.

ContractA.sol
contract ContractA {
function name() external pure returns (string memory) {
return "name";
}

function symbol() external pure returns (string memory) {
return "symbol";
}
}
ContractB.sol
contract ContractB is ContractA {
constructor() { /* ... */ }

function decimals() external pure returns (uint256) {
return 18;
}
}

Then test files should look like this.

ContractA.test.ts
describe("ContractA", () => {
describe("name", () => {
it("should return name properly", async () => { /* ... */ });
});

describe("symbol", () => {
it("should return symbol properly", async () => { /* ... */ });
});
});
ContractB.test.ts
describe("ContractB", () => {
describe("constructor", () => {
it("should set parameters properly", async () => { /* ... */ });
});

describe("decimals", () => {
it("should return decimals properly", async () => { /* ... */ });
});
});

4 Reverterโ€‹

Optimize tests performance by setting up primary contracts once in the top before block, rather than repeating this process before each test using the beforeEach block. Make a snapshot at the end of the before block and reset the blockchain state in the afterEach block using our Reverter tool. This approach ensures a consistent starting blockchain state for each test. We avoid using fixtures that implement similar logic.

import { Reverter } from "@/test/helpers/reverter";

describe("ContractB", () => {
const reverter = new Reverter();

let contractA: ContractA;
let contractB: ContractB;

before(async () => {
const ContractA = await ethers.getContractFactory("ContractA");
const ContractB = await ethers.getContractFactory("ContractB");

contractA = await ContractA.deploy();
contractB = await ContractB.deploy(await contractA.getAddress());

await reverter.snapshot();
});

afterEach(reverter.revert);
});

5 Assertionsโ€‹

For assertions, we utilize the Chai library along with its expect interface. We also incorporate the @nomicfoundation/hardhat-chai-matchers plugin. This plugin extends the capabilities of the expect statement, providing matchers for testing smart contracts.

import { expect } from "chai";

const tx = erc20.mint(OWNER.address, wei(1000));

await expect(tx).to.changeTokenBalance(erc20, OWNER, wei(1000));

6 Other agreementsโ€‹

6.1 Wei

Use the wei function for representing token balances.

import { wei } from "@/scripts/utils/utils";

await erc20.mint(SECOND.address, wei(1000));