Term Introduces: Mock Contracts

Announcement
August 21, 2024

In the development of blockchain applications, testing is crucial to ensure that smart contracts function as intended. However, interacting with real contracts on a live or test network can be time-consuming, expensive, and sometimes impractical. This is where mocking contracts comes into play. By using mock contracts, developers can simulate interactions with smart contracts in a controlled environment, allowing for more efficient and comprehensive testing. This approach helps identify issues early, ensures code reliability, and accelerates the development process.

In this article, we will explore how to mock smart contracts using Viem and Ethers.js—two popular JavaScript libraries for interacting with Ethereum. Whether you’re building a complex dApp or a simple decentralized solution, mastering these techniques will elevate your development workflow and help you deliver robust, reliable applications.

The tutorials below reference the following GitHub repositories:

Let’s dive in and learn how to make your Ethereum development process smoother and more efficient!

Tabs with Copy Code
Viem
Ethers.js

The @term-finance/viem-mock-contract library provides a straightforward way to deploy mock contracts using the hardhat-viem plugin for Hardhat. It supports TypeScript and Yarn, making it an excellent choice for modern blockchain development workflows.

Installation

To install the @term-finance/viem-mock-contract library, you can use either npm or Yarn. Below are the commands for both:

Using npm
npm install --save-dev @term-finance/viem-mock-contract
Using Yarn
yarn add --dev @term-finance/viem-mock-contract

Usage

To use the library, you need to import the deployMock function. This function allows you to deploy a mock contract to the blockchain and set up various expectations for your tests.

Importing deployMock

import { deployMock } from "@term-finance/viem-mock-contract";

Writing Tests with @term-finance/viem-mock-contract

In this section, we'll break down the process of writing tests using the @term-finance/viem-mock-contract library in detail. We'll explore each part of the provided code example to ensure you understand how to deploy and interact with mock contracts effectively.

Importing Required Modules

First, you need to import the necessary modules:

import hre from "hardhat";
import { deployMock } from "@term-finance/viem-mock-contract";

- hre: This is the Hardhat Runtime Environment, providing various utilities and helpers for interacting with the blockchain during testing.

- deployMock: This function from the @term-finance/viem-mock-contract library is used to deploy mock contracts.

Setting Up the Test Suite

Next, set up your test suite using a test framework like Mocha. Here, we define a test suite for a contract named MyContract:

describe("MyContract", () => {

- describe: A function from Mocha to group related tests. It takes a string description and a callback function containing the tests.

Writing an Individual Test

Within the test suite, write individual tests using the it function. Here’s an example test that deploys a mock contract:

it("should deploy a mock contract", async () => {

- it: A function from Mocha to define a single test case. It takes a string description and a callback function with the test logic.

Getting Clients

Use the Hardhat Runtime Environment to get a public client and a wallet client:

const reader = await hre.viem.getPublicClient();
const [signer] = await hre.viem.getWalletClients();

- reader: A client to read data from the blockchain.

- signer: A wallet client used to sign transactions.

Deploying the Mock Contract

Deploy the mock contract using the deployMock function:

const mockContract = await deployMock(signer, reader);

- deployMock: This function deploys a mock contract using the provided signer and reader clients. It returns the deployed mock contract.

Setting Up Mock Expectations

Define expectations for the mock contract using the setup function of the deployMock:

await deployMock.setup(
{
kind: "read",
abi,
inputs: [1n, 2n],
outputs: [3n],
},
{
kind: "revert",
abi,
inputs: [2n, 3n],
reason: "revert reason",
},
// Additional setups can be added here
);

- setup: A function to configure the behavior of the mock contract.

- kind: The type of interaction (e.g., "read" or "revert").

- abi: The ABI of the contract function being mocked.

- inputs: The inputs to the contract function.

- outputs: The expected outputs for "read" interactions.

- reason: The revert reason for "revert" interactions.

Calling the Mock Contract

Interact with the mock contract by calling its functions and checking the results:

const result = await reader.readContract(
mockContract.address,
"myFunction1",
[1, 2],
);

// Check the result
expect(result).to.equal(3);

- readContract: A function to call a contract's read-only function.

- mockContract.address: The address of the mock contract.

- "myFunction1": The name of the function being called.

- [1, 2]: The inputs to the function.

- expect(result).to.equal(3): Asserts that the result of the function call is 3.

Handling Reverts

Test the revert behavior of the mock contract:

try {
await reader.readContract(mockContract.address, "myFunction2", [1, 2]);
assert.fail("Expected revert");
} catch (error) {
expect(error.message).to.contain("revert reason");
}

- try...catch: A block to handle exceptions.

- assert.fail("Expected revert"): Fails the test if the expected revert does not occur.

- expect(error.message).to.contain("revert reason"): Asserts that the revert reason matches the expected reason.

Running Tests

To run your tests, follow these steps:

  1. Install the necessary dependencies:
  2. yarn install
  3. Run the tests:
  4. yarn test

By using the @term-finance/viem-mock-contract library, you can efficiently test your smart contracts in a controlled environment, ensuring that your blockchain applications are robust and reliable. This approach not only saves time and resources but also helps catch potential issues early in the development cycle. Happy coding!


The @term-finance/ethers-mock-contract library is designed to facilitate the deployment of mock contracts using the hardhat-ethers plugin for hardhat. It is an excellent choice for projects looking to upgrade from ethereum-waffle or smock due to its compatibility with ethersjs v6.

Installation

To get started with @term-finance/ethers-mock-contract, you need to install it using either npm or yarn. Here are the commands:

Using npm
npm install --save-dev @term-finance/ethers-mock-contract
Using yarn
yarn add --dev @term-finance/ethers-mock-contract

Usage

Once installed, you can start using the library by importing the deployMock function. Here's a step-by-step guide on how to deploy and interact with a mock contract:

Importing the Library

import { deployMock } from "@term-finance/ethers-mock-contract";

Writing Tests with @term-finance/ethers-mock-contract

In this section, we'll delve deeper into writing tests using the @term-finance/ethers-mock-contract library. We'll break down each part of the test example to explain its functionality and importance.

1. Importing Necessary Modules

First, we need to import the required modules:

import hre from "hardhat";
import { deployMock } from "@term-finance/ethers-mock-contract";

Here, hre is the Hardhat Runtime Environment, which provides all the functionality of Hardhat. The deployMock function from the @term-finance/ethers-mock-contract library is used to deploy mock contracts.

2. Setting Up the Test Suite

Next, we define a test suite using Mocha's describe function and a test case using it:

describe("MyContract", () => {
it("should deploy a mock contract", async () => {
// Test logic goes here
});
});

In this example, the test suite is named "MyContract", and it contains a single test case that checks if a mock contract can be deployed successfully.

3. Getting Signers

We retrieve the signers (accounts) that will interact with the mock contract:

const [signer] = await hre.ethers.getSigners();

This line fetches the list of available signers and destructures the first one. Signers are entities that can send transactions, and they represent different Ethereum accounts.

4. Defining the ABI

The ABI (Application Binary Interface) defines the functions and events of the smart contract. For the mock contract, we need to provide the ABI of the functions we want to mock:

const abi = [
// ABI of the functions to be mocked
];

Replace the comment with the actual ABI of the contract you are mocking.

5. Deploying the Mock Contract

Using the deployMock function, we deploy the mock contract:

const mockContract = await deployMock(abi, signer);

This function takes the ABI and the signer as arguments and returns an instance of the deployed mock contract.

6. Setting Up Expectations

The setup method is used to define the behavior of the mocked functions. It allows you to specify what the mock contract should return when certain functions are called with specific arguments:

await mockContract.setup(
{
kind: "read",
abi: abi[0],
inputs: [1n, 2n],
outputs: [3n],
},
{
kind: "revert",
abi: abi[1],
inputs: [2n, 3n],
reason: "revert reason",
}
);

In this example, we set up two expectations:

  • The first expectation is for a read operation. When the function described by abi[0] is called with inputs 1n and 2n, it should return 3n.
  • The second expectation is for a revert operation. When the function described by abi[1] is called with inputs 2n and 3n, it should revert with the reason "revert reason".
7. Calling the Mock Contract

We then call the mocked function and check the result:

const result = await mockContract.myFunction1(1, 2);
expect(result).to.equal(3);

Here, myFunction1 is the mocked function, and we assert that its return value is 3.

8. Checking for a Revert

We also check that another function call reverts with the expected reason:

try {
await mockContract.myFunction2(2, 3);
assert.fail("Expected revert");
} catch (error) {
expect(error.message).to.contain("revert reason");
}

In this block, we call myFunction2 with inputs 2 and 3. Since we set up an expectation for this function to revert, we check that the error message contains the reason "revert reason".

Complete Example

Here's the complete example with detailed comments:

import hre from "hardhat";
import { deployMock } from "@term-finance/ethers-mock-contract";

describe("MyContract", () => {
it("should deploy a mock contract", async () => {
// Get the list of signers
const [signer] = await hre.ethers.getSigners();

// Define the ABI of the contract to be mocked
const abi = [
// ABI of the functions to be mocked
];

// Deploy the mock contract
const mockContract = await deployMock(abi, signer);

// Set up expectations for the mock contract
await mockContract.setup(
{
kind: "read",
abi: abi[0],
inputs: [1n, 2n],
outputs: [3n],
},
{
kind: "revert",
abi: abi[1],
inputs: [2n, 3n],
reason: "revert reason",
}
);

// Call the mocked function and check the result
const result = await mockContract.myFunction1(1, 2);
expect(result).to.equal(3);

// Check that another function call reverts with the expected reason
try {
await mockContract.myFunction2(2, 3);
assert.fail("Expected revert");
} catch (error) {
expect(error.message).to.contain("revert reason");
}
});
});

Running the Tests

To run your tests, you can use the following commands:

yarn install
yarn test

Conclusion

Mocking contracts is a powerful technique that can significantly improve the robustness of your smart contract development process. The @term-finance/ethers-mock-contract library provides an easy-to-use and highly compatible solution for projects using ethersjs v6. By integrating this library into your testing workflow, you can ensure that your contracts behave as expected under various scenarios, leading to more secure and reliable decentralized applications.