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!
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.
To install the @term-finance/viem-mock-contract
library, you can use either npm or Yarn. Below are the commands for both:
npm install --save-dev @term-finance/viem-mock-contract
yarn add --dev @term-finance/viem-mock-contract
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.
deployMock
import { deployMock } from "@term-finance/viem-mock-contract";
@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.
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.
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.
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.
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.
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.
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.
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
.
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.
To run your tests, follow these steps:
yarn install
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
.
To get started with @term-finance/ethers-mock-contract
, you need to install it using either npm
or yarn
. Here are the commands:
npm install --save-dev @term-finance/ethers-mock-contract
yarn add --dev @term-finance/ethers-mock-contract
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:
import { deployMock } from "@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.
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.
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.
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.
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.
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.
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:
abi[0]
is called with inputs 1n
and 2n
, it should return 3n
.abi[1]
is called with inputs 2n
and 3n
, it should revert with the reason "revert reason".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
.
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".
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");
}
});
});
To run your tests, you can use the following commands:
yarn install
yarn test
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.