Skip to main content
Create your own

Anatomy of an ethers.js Test File

Welcome to the first lesson of our module on "Smart Contract Testing with Hardhat." In our last session, you successfully compiled a Solidity contract and deployed it to a local blockchain network. That process covers the "happy path"—getting your code from your editor onto the chain. Now, we shift our focus to ensuring your code is robust, secure, and behaves correctly under all circumstances.

In your work with investment management software and data warehousing, you know that rigorous testing is non-negotiable. An error in an ETL job or a bug in a portfolio calculation can have serious consequences. In the world of smart contracts, this principle is amplified: because code on the blockchain is immutable and often controls financial assets directly, automated testing is not just a best practice, it is an essential part of the development lifecycle.

This lesson will introduce you to the fundamental structure of a smart contract test file. By the end of our time, you will be able to describe the roles of the key tools in the Hardhat testing ecosystem—Mocha, Chai, and ethers.js—and understand how they work together to form a test.

1. The Testing Trinity: Mocha, Chai, and Ethers.js

When you run npx hardhat test, Hardhat orchestrates a suite of tools to execute your tests. The three most important ones are automatically included when you set up a Hardhat project with the @nomicfoundation/hardhat-toolbox.

  • Mocha: This is a popular JavaScript test framework that provides the basic organizational structure for your tests. It gives us two key functions: describe() to group related tests into a "test suite," and it() to define an individual "test case." Think of describe() as a folder and it() as a file inside it.
  • Chai: This is an assertion library that lets you express what you expect the outcome of your code to be. It provides functions like expect(), which you can chain with "matchers" like .to.equal() or .to.be.reverted. Assertions are the heart of a test; they are the pass/fail checks.
  • Ethers.js: You've encountered this library before. In testing, we use ethers.js as our bridge to the local Hardhat Network. It allows our test scripts to deploy contracts, call their functions, check their state, and simulate transactions from different user accounts (known as "Signers").

These three tools work in concert: Mocha organizes the tests, Ethers.js performs actions on the contract, and Chai verifies that the actions produced the expected results.

2. Anatomy of a Test File

A test file in Hardhat is typically a JavaScript or TypeScript file located in the /test directory of your project. Let's break down its core components.

The article "Hardhat Guide: Automated Smart Contract Testing" from Krayon Digital offers a concise explanation of these building blocks.

Hardhat Guide: Automated Smart Contract Testing

This guide provides a clear breakdown of the fundamental parts of a Hardhat test script.

Please read the section "3. Key parts of automated testing". Focus on the subsections Grouping tests with describe blocks and Writing individual tests with it blocks. These two sections explain the Mocha functions that form the backbone of any test file. You can skim the section on hooks for now, as we'll cover that in detail later.

As you read, you saw that a test file is structured hierarchically:

  1. A top-level describe() block names the contract being tested (e.g., describe("Token contract", ...)).
  2. Inside, one or more it() blocks define individual test cases. Each it() block should have a descriptive name explaining what it tests (e.g., it("Should set the owner correctly", ...)).
  3. Inside each it() block, you write the logic to Arrange your test, Act on the contract, and Assert the outcome.

The official Hardhat documentation provides a simple, complete example that puts all these pieces together.

5. Testing contracts | Ethereum development environment ...

This official tutorial provides the canonical "first test" example for a simple token contract.

Please read the section Writing tests. Pay close attention to the code block and the line-by-line explanation that follows it. It shows you how to: Get a "Signer" (an account) with ethers.getSigners(). Deploy a contract from within the test using ethers.deployContract(). Call a contract function (balanceOf()). Make an assertion with expect().

The example you just read is a perfect illustration of the Arrange-Act-Assert pattern:

  • Arrange: const [owner] = await ethers.getSigners(); and const hardhatToken = await ethers.deployContract("Token"); set up the test environment by getting an account and deploying the contract.
  • Act: const ownerBalance = await hardhatToken.balanceOf(owner.address); performs the action we want to test—calling the balanceOf function.
  • Assert: expect(await hardhatToken.totalSupply()).to.equal(ownerBalance); verifies that the result is correct.

Here is what a more complex test file might look like inside a code editor like VS Code, including the terminal output after a successful run.

This image shows a test file for a "KYC Contract." You can clearly see the `describe` block for the contract, a `beforeEach` hook for setup (which we will cover soon), and several `it` blocks for individual tests like "Should set owner correctly." The terminal below shows the test runner's output, with green checkmarks indicating that all 9 tests passed.

3. Seeing it in Action

Reading about tests is one thing; seeing them written is another. The following video from Alchemy provides an excellent, practical walkthrough of how to set up a test file and write a simple test from scratch.

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

This video demonstrates the process of creating a test file, writing a setup routine, and implementing a test case.

Watch from this segment where the presenter writes the first test. He explains the purpose of the it block, gets the contract instance and the owner's account, and then writes the expect assertion. Then, watch the debugging process. This part is particularly insightful as it shows what happens when an assertion fails and how to fix it, which really clarifies how expect works. For now, you can disregard the details about loadFixture; it's a helper for setting up tests that we will explore in our next lesson.

4. Your First Test

Now, let's apply what you've learned. In the previous lesson, you created a Box.sol contract. We'll write a simple test for it.

  1. In your Hardhat project, navigate to the /test directory. If it doesn't exist, create it.
  2. Create a new file named Box.test.ts.
  3. Add the following code to this file:
import { expect } from "chai";
import { ethers } from "hardhat";

describe("Box Contract", function () {
  it("Should retrieve a value of 0 after deployment", async function () {
    // Arrange: Get the ContractFactory and deploy the contract
    const Box = await ethers.getContractFactory("Box");
    const box = await Box.deploy();

    // Act: Call the retrieve function
    const retrievedValue = await box.retrieve();

    // Assert: Check if the retrieved value is 0
    expect(retrievedValue).to.equal(0);
  });
});

Let's break this down:

  • We import expect from Chai and ethers from Hardhat.
  • We define a test suite for our Box contract with describe.
  • We create one test case with it to check the contract's initial state.
  • Inside the test, we first get a ContractFactory for "Box" and deploy it. This is our Arrange step.
  • Next, we call the retrieve() function. This is our Act step.
  • Finally, we Assert that the value returned is 0, which is the default for an uninitialized uint256 state variable.

Now, run the test from your terminal:
npx hardhat test

You should see output indicating that 1 test has passed. You have successfully written and executed your first automated smart contract test!

Conclusion

In this lesson, you've taken the first and most important step into the world of smart contract testing. You learned how to structure a test file and what roles the core tools—Mocha, Chai, and Ethers.js—play in the process.

Here are the key takeaways:

  • Mocha provides the describe (test suite) and it (test case) functions to structure your tests.
  • Chai provides the expect function to create assertions that verify your contract's behavior.
  • Ethers.js allows your test script to deploy and interact with your smart contracts on the Hardhat Network.
  • The Arrange-Act-Assert pattern is a clear and effective way to structure the logic within each test case.

In our next lesson, we will build directly on this foundation. You'll learn how to write a dedicated setup routine to deploy a fresh contract instance before each test, which keeps your tests clean and independent. We will then move on to writing more complex tests for a contract's functions, including how to assert expected outcomes.

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

Sign up