Skip to main content
Create your own

Deploying Contracts for Testing

In our previous lesson, you wrote your first automated test for the Box contract. You learned how to use Mocha, Chai, and ethers.js to arrange the test, act on the contract, and assert the outcome. In that test, the contract deployment happened inside the it block. While functional for a single test, this approach becomes repetitive and inefficient as your test suite grows.

This lesson focuses on a core principle of robust testing: test isolation. Each test should run in a clean, predictable environment, unaffected by the tests that ran before it. In your work with data warehousing and ETL processes, you understand the importance of starting each job with a known, consistent state. A failed job shouldn't corrupt the data source for the next run. The same concept applies here. We will create a setup routine that deploys a fresh contract instance before each test, ensuring this isolation and making our tests cleaner and more maintainable.

By the end of this lesson, you will be able to write an efficient setup routine using Hardhat's recommended loadFixture helper, a powerful tool for optimizing your testing workflow.

1. The Principle of Test Independence

A fundamental best practice in automated testing is that tests should be independent and runnable in any order. The success or failure of one test should never influence another. When one test modifies the state of a smart contract (e.g., by changing a value or transferring tokens), the next test should not start with that modified state. It should start with a pristine, freshly deployed contract.

We can achieve this by redeploying our contract before every single test. While you could copy and paste the deployment code into each it block, that violates the "Don't Repeat Yourself" (DRY) principle, leading to code that is difficult to maintain. A much better approach is to use a dedicated setup hook.

2. The Traditional Approach: beforeEach

Test frameworks like Mocha provide "hooks," which are functions that run before or after tests. The most common setup hook is beforeEach(). As the name implies, any code inside a beforeEach block is executed before each it block in the same test suite (describe block).

Take a look at the following image showing a test file in VS Code.

This image shows a real-world test file for a KYC contract. Notice the `beforeEach` block on lines 9-17. This code, which deploys fresh instances of the `KYC` and `BondToken` contracts, runs before every single test case (`it` block) listed in the terminal output below.

The Krayon Digital guide you reviewed previously provides a clear code example of this pattern.

Hardhat Guide: Automated Smart Contract Testing

This article section demonstrates how to use the beforeEach hook to deploy a contract.

Please read the subsection Using before and after hooks. Pay close attention to the code block. You'll see that the faucet and owner variables are declared at the top of the describe block and then assigned their values inside the beforeEach hook. This makes them available to all subsequent tests in the suite.

While beforeEach works, it has a significant drawback in the context of blockchain development: it re-executes the deployment transactions for every test. As your contracts and deployment logic become more complex, this can make your test suite very slow.

3. The Hardhat Way: loadFixture

To solve this performance issue, Hardhat provides a highly optimized helper function called loadFixture. It's designed specifically for the task of setting up a test environment efficiently.

Here’s how it works:

  1. You define a special async function, called a "fixture," that contains all your setup logic (e.g., deploying contracts, getting accounts).
  2. The first time you call loadFixture with your fixture function, it executes the setup and takes a "snapshot" of the blockchain's state.
  3. For every subsequent test that calls loadFixture with the same fixture, instead of re-executing the deployment, Hardhat instantly resets the blockchain state to that initial snapshot.

This snapshot-and-reset mechanism is dramatically faster than re-running transactions every time. The Hardhat documentation explains this concept perfectly.

Testing your smart contracts with Ethers and Mocha

This official documentation explains the rationale behind fixtures and provides the canonical example of how to use loadFixture.

Please read the entire section Using fixtures. It starts by showing the beforeEach pattern and its problems, then introduces loadFixture as the solution. Focus on how the deployCounterFixture function is structured and how it's called within the it blocks using networkHelpers.loadFixture.

As you saw, the pattern involves two main parts:

  • The Fixture Function: An async function that performs all the setup and returns an object containing the contract instances, accounts, and any other variables your tests will need.
  • Calling loadFixture: Inside each it block, you call await loadFixture(yourFixture) to get a clean state and access the returned variables.

Let's watch a detailed video walkthrough that implements this pattern from scratch for a more complex contract. This will give you a strong practical understanding.

Hardhat Testing Tutorial | Solidity Smart Contract Testing Developer | Hardhat Testing Course

This video provides a step-by-step guide to writing a complete test file, with a strong focus on creating a reusable setup function and using loadFixture.

First, watch the segment from Creating the setup function. The presenter creates a function called runEveryTime. Notice how this function encapsulates everything needed for the tests: Defining constants (oneYearInSeconds). Calculating the unlockTime. Getting signers (owner, otherAccount). Deploying the MyTest contract. Returning all of these artifacts in a single object. Next, watch how this setup is used from Calling loadFixture. Inside the it block, the presenter calls loadFixture(runEveryTime) to execute the setup and destructures the returned object to get the myTest contract instance and unlockTime variable needed for that specific test. This is the core pattern you will be using.

4. Your Turn: Refactoring the Box Test

Now, let's apply this pattern to refactor the Box.test.ts file you created in the last lesson. We will move the deployment logic into a fixture.

  1. Open your Box.test.ts file.

  2. Import the loadFixture helper from @nomicfoundation/hardhat-network-helpers at the top of your file. Your imports should now look like this:

    import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
    import { expect } from "chai";
    import { ethers } from "hardhat";
    
  3. Inside your describe("Box Contract", ...) block, but before your it block, define your fixture function. This function will handle the deployment.

    async function deployBoxFixture() {
      // Get the ContractFactory and deploy the contract
      const Box = await ethers.getContractFactory("Box");
      const box = await Box.deploy();
    
      // Return the deployed contract instance so it can be used in tests
      return { box };
    }
    
  4. Finally, modify your it block to use this new fixture. Replace the manual deployment lines with a single call to loadFixture.

    it("Should retrieve a value of 0 after deployment", async function () {
      // Arrange: Load the contract instance from our fixture
      const { box } = await loadFixture(deployBoxFixture);
    
      // Act: Call the retrieve function
      const retrievedValue = await box.retrieve();
    
      // Assert: Check if the retrieved value is 0
      expect(retrievedValue).to.equal(0);
    });
    

Your complete test file should now look like this:

import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { expect } from "chai";
import { ethers } from "hardhat";

describe("Box Contract", function () {
  // We define a fixture to reuse the same setup in every test.
  async function deployBoxFixture() {
    // Get the ContractFactory and deploy the contract
    const Box = await ethers.getContractFactory("Box");
    const box = await Box.deploy();

    // Return the deployed contract instance
    return { box };
  }

  it("Should retrieve a value of 0 after deployment", async function () {
    // Arrange: Load the contract instance from our fixture
    const { box } = await loadFixture(deployBoxFixture);

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

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

Run the test again from your terminal: npx hardhat test. The test should still pass, but now your code is much cleaner and ready to be scaled with more test cases.

Conclusion

In this lesson, you've learned how to create a clean, isolated, and efficient setup for your smart contract tests. This is a crucial skill that keeps your test suites fast and maintainable.

Here are the key takeaways:

  • Test Isolation: Each test must begin from a known, consistent state, independent of other tests.
  • Setup Routines: Using a setup routine like beforeEach or a fixture avoids repetitive code.
  • loadFixture: This is Hardhat's optimized helper for test setups. It uses blockchain snapshots to reset the state before each test, which is significantly faster than redeploying contracts every time.
  • The Fixture Pattern: You define an async function to handle deployment and return necessary variables, then call it with loadFixture inside your tests.

Now that you have mastered the art of setting up tests cleanly, our next lesson will focus on the "Act" and "Assert" parts. You will learn how to write a variety of unit tests for a contract's functions, checking for correct return values, state changes, and other expected outcomes.

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

Sign up