Test fixtures are a great way to save time in a smart contract test suite.
Instead of recreating the entire chain state on each test, you can essentially snapshot your chain position, so every time you run a new test case, it starts at the snapshotted state.
One problem, though. As DeFi gets more complex, we’re going to begin calling more and more test fixtures in our contract tests. I’m working on a staking library right now, and that relies on several layers of fixtures.
I spent some time this week trying to figure out how the fixtures do work. Turns out, it’s actually very simple:
import {providers, Wallet} from 'ethers';
import {MockProvider} from './MockProvider';
export type Fixture<T> = (wallets: Wallet[], provider: MockProvider) => Promise<T>;
interface Snapshot<T> {
fixture: Fixture<T>;
data: T;
id: string;
provider: providers.Web3Provider;
wallets: Wallet[];
}
export const loadFixture = createFixtureLoader();
export function createFixtureLoader(overrideWallets?: Wallet[], overrideProvider?: MockProvider) {
const snapshots: Snapshot<any>[] = [];
return async function load<T>(fixture: Fixture<T>): Promise<T> {
const snapshot = snapshots.find((snapshot) => snapshot.fixture === fixture);
if (snapshot) {
await snapshot.provider.send('evm_revert', [snapshot.id]);
snapshot.id = await snapshot.provider.send('evm_snapshot', []);
return snapshot.data;
} else {
const provider = overrideProvider ?? new MockProvider();
const wallets = overrideWallets ?? provider.getWallets();
const data = await fixture(wallets, provider);
const id = await provider.send('evm_snapshot', []);
snapshots.push({fixture, data, id, provider, wallets});
return data;
}
};
}
https://github.com/EthWorks/Waffle/blob/master/waffle-provider/src/fixtures.ts#L21
Ok, so what’s going on here? createFixtureLoader
is a factory that returns a loadFixture
function, bound to a set of wallets and a provider.
The real magic is with the evm_revert
and evm_snapshot
calls.
So if you want to nest fixtures, you have to call them a specific way:
loadFixture(outermostFixture)
loadFixture
: fixture(wallets, provider)
.That way, it only snapshots once.