Skip to main content
Create your own

Testing Initial Contract State

Welcome back. In our last lesson, you successfully created the Solidity smart contract for a fixed-supply ERC-20 token. You defined its name, symbol, and minted the entire supply to the deploying account. This is the "build" phase of development. Now, we must enter the equally crucial "verify" phase.

This lesson focuses on writing automated tests to confirm that your contract's initial state is set up exactly as intended upon deployment. Just as in business intelligence where you write validation rules to ensure data integrity after an ETL process, in smart contract development, we write unit tests to guarantee our code's correctness. We will write tests to verify the token's name, symbol, and total supply, laying the foundation for a robust and reliable tokenized asset.

1. The Importance of Testing and the TDD Mindset

In the world of finance and data, correctness is paramount. An off-by-one error in a financial report can have serious consequences; a similar error in a smart contract can lead to the irreversible loss of millions of dollars. Due to the immutable nature of blockchains, we cannot simply patch a bug after deployment. The code must be as close to perfect as possible before it goes live.

This is why a rigorous testing culture is at the heart of professional smart contract development. Many developers adopt a Test-Driven Development (TDD) methodology:

  1. Red: Write a test that describes a feature and watch it fail (because the feature isn't implemented yet).
  2. Green: Write the minimum amount of code required to make the test pass.
  3. Refactor: Clean up and optimize the code while ensuring the test still passes.

This cycle ensures that every piece of code is written with a specific, testable requirement in mind. For today's lesson, we will apply this thinking to verify the contract you've already built.

2. Anatomy of a Hardhat Test

Hardhat provides an integrated environment for testing your contracts. It uses popular JavaScript libraries:

  • Mocha: A test runner that provides the organizational structure for your tests (the describe and it blocks).
  • Chai: An assertion library that gives you the expressive expect syntax to check for specific outcomes.
  • ethers.js: The library we use to interact with the Ethereum blockchain (or, in this case, Hardhat's local test network).

Your tests will live in the test directory of your Hardhat project.

A typical Hardhat project structure. The `Token.sol` contract resides in the `contracts` folder, while its corresponding test file, `token.js`, is placed in the `test` folder.

3. Writing Your First State Verification Tests

Let's begin by writing tests for the three public-facing "view" functions that define our token's identity: name(), symbol(), and totalSupply(). We will specify what we expect the outcomes to be and then write code to verify those expectations.

The following article provides a fantastic, practical example of writing these initial tests. It follows the TDD approach, which is an excellent practice to learn.

Solidity Tutorial: Create an ERC20 Token - Medium

This article by Cyrille gives a clear, step-by-step guide on creating and testing an ERC-20 token with Hardhat. We will focus on the testing part, which perfectly aligns with our goal for this lesson.

Start by reading the section under the heading "Test Cases". This introduces the testing frameworks and the TDD philosophy. Next, focus on the code provided in the article. You'll find it within a describe("deploy", function () { ... }) block. Study the three it blocks that test the name, symbol, and total supply. Pay close attention to the structure: it("should be named OnMyChain", ...): This defines a single test case with a descriptive name. expect(await token.name()).to.eq("OnMyChain"): This is the core logic. It calls the name() function on the deployed contract and asserts that the result is equal to (.to.eq()) the expected string. Notice the test for totalSupply. It uses ethers.utils.parseEther("100000"). This is a utility from an older version of ethers.js. In the current version bundled with Hardhat, the syntax is ethers.parseEther("100000"). This function is essential for converting a human-readable token amount into the large integer representation used on-chain (i.e., appending 18 zeros).

Based on the article and the MyToken contract from our previous lesson, let's establish the specifications for our token:

  1. Name: MyToken
  2. Symbol: MTK
  3. Total Supply: 1,000,000 tokens.

Here is what the corresponding tests would look like in a file named test/MyToken.test.js.

const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("MyToken", function () {
  let Token;
  let myToken;
  let owner;
  const initialSupply = ethers.parseEther("1000000"); // 1 million tokens with 18 decimals

  // Before each test, we deploy a new contract instance
  beforeEach(async function () {
    [owner] = await ethers.getSigners();
    Token = await ethers.getContractFactory("MyToken");
    myToken = await Token.deploy(initialSupply);
  });

  describe("Deployment", function () {
    it("Should have the correct name", async function () {
      expect(await myToken.name()).to.equal("MyToken");
    });

    it("Should have the correct symbol", async function () {
      expect(await myToken.symbol()).to.equal("MTK");
    });

    it("Should have the correct total supply", async function () {
      expect(await myToken.totalSupply()).to.equal(initialSupply);
    });

    it("Should assign the total supply to the deployer", async function () {
        expect(await myToken.balanceOf(owner.address)).to.equal(initialSupply);
    });
  });
});

Let's break down this structure:

  • beforeEach: This is a Mocha hook that runs before every it block inside its describe scope. It's perfect for setting up a clean state for each test. Here, we deploy a fresh MyToken contract every time, ensuring that tests don't interfere with each other.
  • ethers.getSigners(): This Hardhat helper returns an array of test accounts. The first one (owner) is used by default to deploy the contract.
  • it("Should assign the total supply..."): This is a crucial fourth test. It doesn't just check the totalSupply, but confirms that all those newly minted tokens were correctly assigned to the owner (the deployer), as we designed in the constructor.

4. A Dynamic Look at the Testing Workflow

Reading code is one thing; seeing it in action solidifies understanding. The following video provides an excellent walkthrough of the unit testing process in Hardhat. While the example contract is different (a "Faucet"), the principles, commands, and debugging techniques are identical.

How to unit test a smart contract using Hardhat - Alchemy University

This Alchemy University tutorial demonstrates the hands-on process of setting up and running a Hardhat test.

Please watch these key segments: Test Setup (01:29 - 04:05): See how to take a sample test file, rename it, and clean it up for a new contract. The video also introduces the loadFixture pattern, which is a more efficient version of the beforeEach hook we used above. Writing an Initial State Test (05:59 - 09:14): Watch how a test is written to verify that the owner state variable is set correctly in the constructor. This is directly analogous to our test for the deployer's initial balance. Debugging a Common Error (09:59 - 13:01): This is a very valuable part. The test fails because it tries to compare a full "signer object" with a simple "address string". You will encounter this exact issue. Seeing how to fix it by using owner.address is a critical practical lesson.

To run the tests you've written for MyToken, you would navigate to your project's root directory in the terminal and execute:
npx hardhat test

Hardhat will automatically find all files in the test directory ending in .js or .ts, compile your contracts if needed, and run the tests. You should see a satisfying output with green checkmarks indicating that all your assertions passed.

Conclusion

In this lesson, you've made a significant leap from just building a contract to proving its correctness. You've learned how to structure tests in a Hardhat project, write specific assertions for initial state variables, and run the test suite. This practice of writing automated checks is a cornerstone of professional software engineering and is absolutely essential for the high-stakes environment of blockchain development.

Key Takeaways:

  • Test Structure: Hardhat tests use Mocha's describe for grouping tests and it for individual test cases.
  • Test Setup: The beforeEach hook is used to deploy a clean contract instance for each test, ensuring test isolation.
  • Assertions: Chai's expect syntax provides a readable way to assert that actual results match expected values (e.g., expect(await myToken.name()).to.equal("MyToken")).
  • Handling Token Amounts: The ethers.parseEther() function is the standard tool for converting human-readable decimal strings into the large integer format required by the EVM.
  • Verifying State: The most basic and important tests verify the contract's initial state at the moment of deployment, such as its name, symbol, total supply, and initial owner balances.

You have now built a token contract and verified its deployment state. In the next lesson, we will expand our test suite to cover the token's dynamic behavior: transferring tokens between accounts.

Can't find a good explanation? Sign up and we'll make it for you

Sign up