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: UNLICENSEDpragma 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.tsimport { 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)