Hardhat

Learn how to test on anvil-zksync with Hardhat.

In the world of decentralized applications, the margin for error is remarkably narrow. A single mistake in a contract can have catastrophic implications. For those seeking an efficient method to test and refine their contracts, this guide showcases how to utilize Hardhat and anvil-zksync for all testing needs.

To test our contract, we are going to use Hardhat and anvil-zksync for rapid local development. In our tests we're going to use zksync-ethers to interact with the Greeter contract, and we'll use Mocha as our test runner.

Prerequisites


Environment setup

  1. Create a new project with the required dependencies and boilerplate paymaster implementations:
    zksync-cli create test-greeter --template hardhat_solidity
    

    Use the private key below to use a pre-configured rich wallet for deployment and testing:
    ? Private key of the wallet responsible for deploying contracts (optional)
    0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110
    

    The contract for this guide exists under /contracts/Greeter.sol.
  2. Install the dependencies:
    npm install
    
  3. Compile the contracts:
    npm run compile
    

Run tests with Hardhat

Under the /test directory there is a greeter.test.ts file. The initial test checks if our Greeter contract returns the set greeting.

/test/greeter.test.ts
import { expect } from 'chai';

import * as hre from 'hardhat';

describe('Greeter', function () {
  it("Should return the new greeting once it's changed", async function () {
    // Get contract factory and deploy
    const Greeter = await hre.ethers.getContractFactory('Greeter');
    const greeting = 'Hello world!';
    const greeter = await Greeter.deploy(greeting);
    await greeter.waitForDeployment();

    expect(await greeter.greet()).to.eq(greeting);

    const newGreeting = 'Hola, mundo!';
    const setGreetingTx = await greeter.setGreeting(newGreeting);

    // wait until the transaction is processed
    await setGreetingTx.wait();

    expect(await greeter.greet()).to.equal(newGreeting);
  });
});

To run this test:

npx hardhat test test/greeter.test.ts --network hardhat

You should see the following output:

  Greeter
 Should return the new greeting once it's changed (174ms)

  1 passing (174ms)

Expand test coverage

Our aim is comprehensive coverage. Here are the test scenarios we will cover:

  1. Testing greet() function: Check the returned greeting.
  2. Testing setGreeting() function: Verify the ability to update greetings.
  3. Testing Insufficient Funds: Ensure transactions fail without enough funds.

Each of these test cases will rely on a common setup, which involves creating a provider connected to the ZKsync Sepolia Testnet, initializing a wallet with a known private key, and deploying the Greeter contract.

Let's refactor our test file with the provided script:

To run this test:

npx hardhat test test/greeter.test.ts --network hardhat

You should see the following output:

  Greeter
 Should return the new greeting once it's changed (211ms)
    ✔ Should set a new greeting and return it (2682ms)
    ✔ Should fail when insufficient funds (299ms)

  3 passing (6s)

Understanding the test file

Have a look at the test/greeter.test.ts file's imports:

test/greeter.test.ts
import { expect } from 'chai';
import { Wallet, type Contract } from 'ethers';

import * as hre from 'hardhat';

This section imports all necessary utilities and configurations needed to run our tests.

  • expect from Chai provides assertion functionalities for our tests.
  • Wallet, and the Contract type from ethers help us creating a wallet and defining a contract's type.
  • hre gives us hardhat specific functionalities for deploying and interacting with our contract.

Main Test Suite

describe('Greeter', function () {
  ...
});

Here, we've declared our main test suite. Each test or nested suite inside provides specific scenarios or functionalities we want to test regarding the Greeter contract.

  1. Initialization
    Before running any test, we initialize commonly used variables like the provider, wallet, deployer, and the greeter contract.
  2. Test greet() function
    We check that the greet function returns the initial greeting of 'Hi' after deployment.
    it("Should return the new greeting once it's changed", async function () { ... });
    
  3. Test setGreeting() function
    We test that setting a new greeting updates the contract's state as expected.
    it("Should set a new greeting and return it", async function () { ... });
    
  4. Test insufficient funds
    Here, we simulate a scenario where an empty wallet (with no funds) tries to set a new greeting. We make use of the connect method on your zksync-ethers Contract object to connect it to a different account.
    it("Should fail when insufficient funds", async function () { ... });
    

Made with ❤️ by the ZKsync Community