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," andit()to define an individual "test case." Think ofdescribe()as a folder andit()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:
- A top-level
describe()block names the contract being tested (e.g.,describe("Token contract", ...)). - Inside, one or more
it()blocks define individual test cases. Eachit()block should have a descriptive name explaining what it tests (e.g.,it("Should set the owner correctly", ...)). - 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();andconst 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 thebalanceOffunction. - 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.

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.
- In your Hardhat project, navigate to the
/testdirectory. If it doesn't exist, create it. - Create a new file named
Box.test.ts. - 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
expectfrom Chai andethersfrom Hardhat. - We define a test suite for our
Boxcontract withdescribe. - We create one test case with
itto check the contract's initial state. - Inside the test, we first get a
ContractFactoryfor "Box" anddeployit. 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 uninitializeduint256state 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) andit(test case) functions to structure your tests. - Chai provides the
expectfunction 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.