Writing and Testing Contracts

Writing Contracts

In this tutorial, we’ll create a super-simple smart contract with a simple function of minting tokens to unlimited recipients. The contract will have a simple function for minting tokens, without any other functions of ERC-721 implemented. Here’s what the functionality will be:

  • Minting tokens to unlimited recipients with ease.

To simplify the implementation process, we'll use OpenZeppelin. You can install @openzeppelin/contracts by running the following command:

npm install @openzeppelin/contracts

Now, create a directory named contracts and create a file named Token.sol. Once you have created the file, simply copy and paste the following code into Token.sol:

// File: contracts/Token.sol
/// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.12;
import '@openzeppelin/contracts/token/ERC721/ERC721.sol';
import '@openzeppelin/contracts/access/Ownable.sol';
import '@openzeppelin/contracts/utils/Counters.sol';
contract Token is ERC721, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
constructor() ERC721('My Basic Token', 'MBT') Ownable() {}
function mint(address who) public returns (uint256) {
uint256 tokenId = _tokenIds.current();
_mint(who, tokenId);
_tokenIds.increment();
return tokenId;
}
}

As mentioned earlier, this contract has only one functionality - minting tokens. If you take a look at the mint function, you'll notice that it retrieves the tokenId from the _tokenIds and mints the token using the _mint function.

Compiling Contracts

To compile the written contracts, simply run the command npx hardhat compile in your terminal. This command will compile the contracts and generate files such as ABI and bytecode.

npx hardhat compile
Generating typings for: 13 artifacts in dir: typechain-types for target: ethers-v5 Successfully generated 38 typings! Compiled 13 Solidity files successfully

Testing Contracts

Deploying Contracts for Testing

import { ethers } from 'hardhat'

To test the smart contract, we’ll use the Hardhat-flavored ethers.js library. By importing ethers from Hardhat, we can access a range of functionalities including getContractFactory and getSigners, etc.

const Token = await ethers.getContractFactory('Token')

To obtain the contract factory for the written contracts, we can use the getContractFactory method from ethers. Keep in mind that in this case, Token serves as the factory for the contracts.

const token = await Token.deploy()
await token.deployed()

To deploy the contract for testing purposes, you can use the deploy method of Token. Simply use await to get an instance of the contract, and then wait for the deployment confirmation by calling the deployed method.

To create a testing fixture, we can combine these steps into a single function:

import { ethers } from 'hardhat'
const deployFixture = async () => {
const Token = await ethers.getContractFactory('Token')
const token = await Token.deploy()
await token.deployed()
return { token }
}

Writing Testing Parts

import { expect } from 'chai'
import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'

In this tutorial, we’ll be using chai as our testing framework, but you can use mocha instead. To ensure that the function is only run once in the network as a fixture, we’ll use loadFixture, which is a handy helper function from Hardhat.

const { token } = await loadFixture(deployFixture)

To get an instance of the deployed contract, you can use the loadFixture and deployFixture functions. Note that we have defined the deployFixture function earlier in this tutorial.

const [account] = await ethers.getSigners()

For testing purposes, you can obtain a Signer through the getSigners method of ethers. A Signer is an object that represents an account on the Ethereum network and is used to send transactions.

await token.mint(account.address)

Then, we can send mint transaction with account’s address.

import { expect } from 'chai'
await expect(token.mint(account.address)).to.changeTokenBalance(
token,
account.address,
1
)

Finally, we can use chai to test whether the mint function is executed correctly.

We can combine all the aforementioned steps into a test template.

import { expect } from 'chai'
import { ethers } from 'hardhat'
import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'
// ...
describe('Token', function () {
describe('Mint', function () {
it('Should mint for account to be success', async function () {
const { token } = await loadFixture(deployFixture)
const [account] = await ethers.getSigners()
await expect(token.mint(account.address)).to.changeTokenBalance(
token,
account.address,
1
)
})
})
})

Combining Tests and Fixtures All at Once

To test the contracts, we can put all the previously mentioned functions into a single file. Start by creating a directory named test inside the project root directory, and then create a new file named Token.ts inside test. You can then paste the following code into Token.ts:

// File: test/Token.ts
import { expect } from 'chai'
import { ethers } from 'hardhat'
import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'
describe('Token', function () {
const deployFixture = async () => {
const Token = await ethers.getContractFactory('Token')
const token = await Token.deploy()
await token.deployed()
return { token }
}
describe('Mint', function () {
it('Should mint for account to be success', async function () {
const { token } = await loadFixture(deployFixture)
const [account] = await ethers.getSigners()
await expect(token.mint(account.address)).to.changeTokenBalance(
token,
account.address,
1
)
})
})
})

To test the contracts, run the following command:

npx hardhat test

Here is the expected output of the test:

Token Mint ✔ Should mint for account to be success (5006ms) 1 passing (5s)