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 era_test_node
for all testing needs.
To test our contract, we are going to use Hardhat and era_test_node
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
zksync-cli
installed from the zksync-cli section.era_test_node
installed and running. See In-memory Node.
Environment setup
- Create a new project with the required dependencies and boilerplate paymaster implementations:
zksync-cli create test-greeter
ChooseHardhat + Solidity
to setup the project repository. The contract for this guide exists under/contracts/Greeter.sol
.
Install dependencies:yarn install
- Add the following additional dependencies:
yarn add -D @nomicfoundation/hardhat-chai-matchers @nomiclabs/hardhat-ethers
- Import
@nomicfoundation/hardhat-chai-matchers
into thehardhat.config.ts
file:hardhat.config.tsimport "@nomicfoundation/hardhat-chai-matchers";
The@nomicfoundation/hardhat-chai-matchers
plugin adds Ethereum specific capabilities to the Chai assertion library for testing smart contracts. - Start
era_test_node
:./target/release/era_test_node run
Run tests with Hardhat
Under the /test
directory there is a main.test.ts
. The initial test checks if our Greeter
contract returns the set greeting.
import { expect } from "chai";
import { Wallet, Provider, Contract } from "zksync-ethers";
import * as hre from "hardhat";
import { Deployer } from "@matterlabs/hardhat-zksync";
import { zkSyncTestnet } from "../hardhat.config";
const RICH_WALLET_PK = "0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110";
async function deployGreeter(deployer: Deployer): Promise<Contract> {
const artifact = await deployer.loadArtifact("Greeter");
return await deployer.deploy(artifact, ["Hi"]);
}
describe("Greeter", function () {
it("Should return the new greeting once it's changed", async function () {
const provider = new Provider(zkSyncTestnet.url);
const wallet = new Wallet(RICH_WALLET_PK, provider);
const deployer = new Deployer(hre, wallet);
const greeter = await deployGreeter(deployer);
expect(await greeter.greet()).to.eq("Hi");
const setGreetingTx = await greeter.setGreeting("Hola, mundo!");
// wait until the transaction is mined
await setGreetingTx.wait();
expect(await greeter.greet()).to.equal("Hola, mundo!");
});
});
To run this test:
yarn test
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:
- Testing greet() function: Check the returned greeting.
- Testing setGreeting() function: Verify the ability to update greetings.
- Testing Insufficient Funds: Ensure transactions fail without enough funds.
- Event Emission: Ensure an event is emitted when changing the greeting.
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:
yarn test
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)
✔ Should emit an event when the greeting is changed (2939ms)
4 passing (6s)
Understanding the test file
Have a look at the test/main.test.ts
file's imports:
import { expect } from "chai";
import { Wallet, Provider, Contract } from "zksync-ethers";
import * as hre from "hardhat";
import { Deployer } from "@matterlabs/hardhat-zksync";
import { zkSyncTestnet } from "../hardhat.config";
This section imports all necessary utilities and configurations needed to run our tests.
expect
from Chai provides assertion functionalities for our tests.Wallet
,Provider
, andContract
fromzksync-ethers
help us with ZKsync functionalities like creating wallets and interacting with contracts.hre
andDeployer
give us hardhat specific functionalities for deploying and interacting with our contract.zkSyncTestnet
from our hardhat configuration provides network details of our runningera_test_node.
Contract Deployment Utility
async function deployGreeter(deployer: Deployer): Promise<Contract> { ... }
This utility function simplifies deploying the Greeter contract for our tests.
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.
- Initialization
Before running any test, we initialize commonly used variables like the provider, wallet, deployer, and the greeter contract. - 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 () { ... });
- 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 () { ... });
- 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 theconnect
method on yourzksync-ethers
Contract object to connect it to a different account.it("Should fail when insufficient funds", async function () { ... });
- Test event emission
We test the emission of an event when the greeting changes in the contract making use of thehardhat-chai-matchers
.it("Should emit an event when the greeting is changed", async function () { ... });