Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Setting Up Your Hardhat Testing Environment

Hello! Welcome back to the course.

In the last module, we successfully built, deployed, and verified our ERC-20 token on a public testnet. We manually confirmed it works by transferring tokens between wallets on Etherscan. While this is a great milestone, it's not a scalable or professional way to ensure code quality. In backend development, you wouldn't deploy a new service to production after just a few manual clicks; you'd have a suite of automated unit and integration tests. The same principle applies here, and it's even more critical because smart contracts are immutable once deployed.

This lesson marks the beginning of our module on Automated Testing. We'll shift from manual checks on a live network to writing repeatable, automated tests that run locally. Today, our focus is on setting up the entire testing workbench.

By the end of this lesson, you will be able to:

  • Set up a new Hardhat project configured for automated testing.
  • Understand the roles of the core testing tools: Hardhat, ethers.js, and the Chai assertion library.
  • Recognize the standard project structure for testing.
  • Create the basic skeleton of a test file, ready for writing tests.

1. The Professional Testing Toolkit

In the world of Ethereum development, a few tools have become the industry standard for testing smart contracts. Your backend experience will make these concepts feel very familiar.

  • Hardhat: This is our main development environment and will also act as our test runner. It's analogous to using a framework like Node.js with a test runner like Jest or Mocha. Hardhat orchestrates the entire testing process, from compiling your contracts to running the tests against a simulated blockchain.
  • Hardhat Network: A built-in, local Ethereum network designed for development. Think of it as a mock server or an in-memory database. It starts up instantly, is incredibly fast, and gives you a clean slate for every test run, ensuring your tests are isolated and predictable.
  • Ethers.js: The library we use to interact with the Ethereum blockchain (in our case, the local Hardhat Network). It allows our test scripts (written in TypeScript/JavaScript) to deploy contracts, call their functions, and check their state. This is similar to how you might use a database client library (like pg for PostgreSQL) to interact with a database from your backend code.
  • Mocha: A popular JavaScript test framework that provides the structure for our tests. It gives us functions like describe() to group related tests and it() to define individual test cases.
  • Chai: An assertion library that provides expressive functions to check if our test outcomes are what we expect. Using expect(value).to.equal(anotherValue) is much more readable than writing if (value !== anotherValue) throw new Error(...).

Fortunately, you don't need to install and configure all of these individually. The hardhat-toolbox plugin, which we'll install next, bundles them all together for a seamless experience.


2. Initializing Your Testing Project

While you have an existing project from the last module, we will create a fresh Hardhat project for our testing suite. This ensures we start with the correct, clean configuration recommended for testing.

The official Hardhat documentation is the best reference for these initial steps. We'll walk through it together.

Getting started with Hardhat | Ethereum development ...

Let's start by following the official Hardhat 'Getting started' guide to create a new project.

First, create a new empty directory for this project (e.g., my-token-tests). Navigate into it with your terminal. Follow the instructions under the 'Installation' section to initialize an npm project (npm init -y) and install Hardhat (npm install --save-dev hardhat). Then, proceed to the 'Quick Start' section and run npx hardhat init.

When you run npx hardhat init, you'll be presented with a few options. Since you're comfortable with TypeScript, let's select that.

$ npx hardhat init

👷 Welcome to Hardhat vX.X.X 👷‍

? What do you want to do? …
  Create a JavaScript project
❯ Create a TypeScript project
  Create an empty hardhat.config.js
  Quit

Follow the prompts, accepting the defaults. It will ask if you want to install @nomicfoundation/hardhat-toolbox. Say yes! This is the key package that installs Ethers, Chai, and other testing utilities for us.

After the installation is complete, your project is ready.


3. Exploring the Project Structure

Your new project directory now contains several folders and files. Let's focus on the ones relevant to testing. The following video gives a great overview of this structure.

Build and Deploy Smart Contracts using Hardhat | [See Description]

Patrick Collins from Chainlink provides a clear walkthrough of the standard Hardhat project structure. This will help you understand where everything goes.

Watch from 02:59 to 04:13. Pay attention to his explanation of the contracts, scripts, and especially the test folders.

Here's a breakdown:

  • contracts/: This is where your Solidity source code lives. The sample project creates a Lock.sol. You can delete this file and copy your MyToken.sol file from your previous project into this folder.
  • test/: This is our main focus. It's where all our test files will go. Hardhat automatically looks for and runs files in this directory when you execute the test command. The sample project includes a Lock.ts. We will replace this with our own test file.
  • hardhat.config.ts: The central configuration file. This is where you define which Solidity compiler version to use, configure network connections (like we did for Sepolia), and manage plugins. The hardhat-toolbox is already imported and configured for you.
  • package.json: Manages your project's dependencies, including Hardhat and the testing toolbox.

Now, run the default test command to see it in action:

npx hardhat test

You'll see Hardhat compile the Lock.sol contract and then run the tests from test/Lock.ts, printing a "passing" summary. This confirms that your testing environment is working correctly!

Decentralized Application (dApp) Architecture and Learning Objectives
This diagram shows the full picture of dApp development. Our focus in this module is the 'Test' phase, using Hardhat to ensure our Solidity 'Smart Contract' is correct before it ever gets deployed.

4. The Anatomy of a Test File

Now, let's set up the file where we will write the tests for our own MyToken contract.

  1. Delete the sample test file: test/Lock.ts.
  2. Create a new file: test/MyToken.test.ts.

Inside this file, we will write the basic structure for our test suite. This involves importing the necessary tools and setting up the test blocks.

// Import utilities from Hardhat and Chai
import { expect } from "chai";
import { ethers } from "hardhat";

// `describe` is a container for a group of related tests.
// Here, we group all tests for the MyToken contract.
describe("MyToken", function () {

  // This is a placeholder for our first test.
  // `it` defines an individual test case. The string describes what the test should do.
  it("Should correctly deploy and set the total supply", async function () {
    // Test logic will go here in the next lesson.
  });

  // We can add more 'it' blocks for other tests.
  it("Should have the correct name and symbol", async function () {
    // Another test's logic will go here.
  });

});

Let's break this down:

  • import { expect } from "chai";: This imports the expect function from the Chai assertion library. We'll use this to make claims about our contract's state (e.g., expect(await myToken.name()).to.equal("MyToken")).
  • import { ethers } from "hardhat";: This imports the Ethers.js library, fully integrated with Hardhat. We'll use this to deploy our contract and interact with it.
  • describe("MyToken", ...): This function from Mocha creates a test suite. All the tests related to the MyToken contract will go inside this block. You can nest describe blocks to further organize your tests (e.g., a sub-group for transfer-related tests).
  • it("Should...", async function () { ... }): This defines a single test case. The first argument is a human-readable string explaining what this specific test verifies. The function is marked async because nearly all interactions with a blockchain are asynchronous operations.
Test your understanding!

A colleague writes a test file but forgets the async keyword on their it block's function, like this: it("Should transfer tokens", function() { ... }). Inside the function, they use await to call a contract function. What will happen when they run the test?

Show answer

The test will fail with a syntax error: SyntaxError: await is only valid in async functions. JavaScript requires the async keyword on any function that uses await within it. Since all contract interactions via Ethers.js return Promises that need to be awaited, virtually all Hardhat test it blocks must be async.


Conclusion

Excellent work! You have successfully set up a professional, industry-standard environment for writing automated tests for your smart contracts. While we haven't written any actual test logic yet, preparing the workshop is a critical first step.

Key Takeaways:

  • Automated testing is crucial for smart contract security and reliability.
  • The @nomicfoundation/hardhat-toolbox package provides an all-in-one solution, bundling Hardhat, the Hardhat Network, Ethers.js, Mocha, and Chai.
  • Hardhat projects follow a standard structure, with contracts in contracts/ and tests in test/.
  • Tests are organized into describe blocks (test suites) and it blocks (individual test cases).
  • Test functions must be async to handle the asynchronous nature of blockchain interactions.

Preview of the Next Lesson:
With our environment fully configured, we're ready to get to work. In the next lesson, we will populate our MyToken.test.ts file and write our first unit tests. We'll start by testing the most fundamental functions of our ERC-20 token: transfer() and balanceOf().

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

Sign up