Testing
Testing your smart contracts is essential to make sure they work correctly before deploying to a real blockchain. Skittles works with Hardhat, a popular development tool that lets you run tests against a local simulated blockchain.
How It Works
- Compile your TypeScript contracts with Skittles
- Run tests with Hardhat against a local simulated blockchain
Your package.json test script should run both steps:
{
"scripts": {
"test": "skittles compile && hardhat test"
}
}
What skittles init Sets Up
When you run skittles init, we scaffold a project that includes:
- Hardhat configured with testing tools
- A sample test file demonstrating event assertions, revert checking, and fixtures
- Scripts and dev dependencies for the full testing workflow
Recommended Configuration
The init template uses the configuration recommended in the Hardhat v3 testing guide:
- Test runner: Mocha for organizing and running your tests
- Contract interactions: ethers.js v6 for interacting with your contracts
- Assertions: Chai matchers for checking events, reverts, and custom errors
- Fixtures: Fast test setup that saves time when running multiple tests
Common Testing Patterns
Below are some common smart-contract-specific testing patterns. All examples assume you have a fixture like this:
import "@nomicfoundation/hardhat-ethers";
import "@nomicfoundation/hardhat-ethers-chai-matchers";
import "@nomicfoundation/hardhat-network-helpers";
import { expect } from "chai";
import hre from "hardhat";
const { ethers, networkHelpers } = await hre.network.connect();
async function deployFixture() {
const contract = await ethers.deployContract("Staking");
const [owner, alice, bob] = await ethers.getSigners();
return { contract, owner, alice, bob };
}
Testing Payable Functions
To send ETH when calling a function, pass a value option. This is how you test functions that accept ETH payments:
it("accepts ETH deposits", async function () {
const { contract, alice } = await networkHelpers.loadFixture(deployFixture);
const addr = await contract.getAddress();
const contractAsAlice = await ethers.getContractAt("Staking", addr, alice);
await contractAsAlice.deposit({ value: ethers.parseEther("1.0") });
expect(await contract.getDeposit(alice.address)).to.equal(
ethers.parseEther("1.0")
);
});
Testing ETH Balance Changes
Use ethers.provider.getBalance to check how a contract's (or an account's) ETH balance changes after a transaction:
it("tracks contract ETH balance on deposit", async function () {
const { contract, alice } = await networkHelpers.loadFixture(deployFixture);
const addr = await contract.getAddress();
const contractAsAlice = await ethers.getContractAt("Staking", addr, alice);
const balBefore = await ethers.provider.getBalance(addr);
await contractAsAlice.deposit({ value: ethers.parseEther("1.0") });
const balAfter = await ethers.provider.getBalance(addr);
expect(balAfter - balBefore).to.equal(ethers.parseEther("1.0"));
});
Testing Custom Errors
Use revertedWithCustomError to assert that a transaction reverts with a specific custom error declared in your contract:
it("reverts with custom error", async function () {
const { contract, alice } = await networkHelpers.loadFixture(deployFixture);
const addr = await contract.getAddress();
const contractAsAlice = await ethers.getContractAt("Staking", addr, alice);
await expect(
contractAsAlice.withdraw(ethers.parseEther("999"))
).to.be.revertedWithCustomError(contract, "InsufficientDeposit");
});
For simple string error messages, use revertedWith:
it("reverts with message", async function () {
const { contract, alice } = await networkHelpers.loadFixture(deployFixture);
const addr = await contract.getAddress();
const contractAsAlice = await ethers.getContractAt("Staking", addr, alice);
await expect(
contractAsAlice.deposit({ value: 0 })
).to.be.revertedWith("Must send ETH");
});
Testing with Different Signers
Use ethers.getContractAt with a different signer to call contract functions as another account. This is useful for testing access control:
it("only allows owner to pause", async function () {
const { contract, alice } = await networkHelpers.loadFixture(deployFixture);
const addr = await contract.getAddress();
const contractAsAlice = await ethers.getContractAt("Staking", addr, alice);
await expect(
contractAsAlice.pause()
).to.be.revertedWithCustomError(contract, "NotOwner");
});
Learn More
For detailed testing patterns, matchers, multichain support, and advanced usage, see the official Hardhat docs: