https://img.shields.io/npm/v/@defi-wonderland/smock.svg?style=flat-square https://badgen.net/badge/icon/discord?icon=discord&label

Smock is the Solidity mocking library. It’s a plugin for hardhat that can be used to create mock Solidity contracts entirely in JavaScript (or TypeScript!). With Smock, it’s easier than ever to test your smart contracts. You’ll never have to write another mock contract in Solidity again.

Smock is inspired by sinon, sinon-chai, and Python’s unittest.mock. Although Smock is currently only compatible with hardhat, we plan to extend support to other testing frameworks like Truffle.

If you wanna chat about the future of Solidity Mocking, join our Discord!

Features

  • Get rid of your folder of “mock” contracts and just use JavaScript.

  • Keep your tests simple with a sweet set of chai matchers.

  • Fully compatible with TypeScript and TypeChain.

  • Manipulate the behavior of functions on the fly with fakes.

  • Modify functions and internal variables of a real contract with mocks.

  • Make assertions about calls, call arguments, and call counts.

  • We’ve got extensive documentation and a complete test suite.

Documentation

Detailed documentation can be found here.

Quick Start

Installation

You can install Smock via npm or yarn:

npm install @defi-wonderland/smock

Basic Usage

Smock is dead simple to use. Here’s a basic example of how you might use it to streamline your tests.

...
import { FakeContract, smock } from '@defi-wonderland/smock';

chai.should(); // if you like should syntax
chai.use(smock.matchers);

describe('MyContract', () => {
    let myContractFake: FakeContract<MyContract>;

    beforeEach(async () => {
        ...
        myContractFake = await smock.fake('MyContract');
    });

    it('some test', () => {
        myContractFake.bark.returns('woof');
        ...
        myContractFake.bark.atCall(0).should.be.calledWith('Hello World');
    });
});

License

Smock is released under the MIT license. Feel free to use, modify, and/or redistribute this software as you see fit. See the LICENSE file for more information.

Contributors

Maintained with love by Optimism PBC and DeFi Wonderland. Made possible by viewers like you.

Getting Started

Installation

yarn add --dev @defi-wonderland/smock

Required Config for Mocks

Mocks allow you to manipulate any variable inside of a smart contract. If you’d like to use mocks, you must update your hardhat.config.<js/ts> file to include the following:

// hardhat.config.js

... // your plugin imports and whatnot go here

module.exports = {
  ... // your other hardhat settings go here
  solidity: {
    ... // your other Solidity settings go here
    compilers: [
      ...// compiler options
      settings: {
        outputSelection: {
          "*": {
            "*": ["storageLayout"]
          }
        }
      }
    ]
  }
}

Optional config to use Smock Matchers

Fakes

What are fakes?

Fakes are JavaScript objects that emulate the interface of a given Solidity contract. You can use fakes to customize the behavior of any public method or variable that a smart contract exposes.

When should I use a fake?

Fakes are a powerful tool when you want to test how a smart contract will interact with other contracts. Instead of initializing a full-fledged smart contract to interact with, you can simply create a fake that can provide pre-programmed responses.

Fakes are especially useful when the contracts that you need to interact with are relatively complex. For example, imagine that you’re testing a contract that needs to interact with another (very stateful) contract. Without smock, you’ll probably have to:

  1. Deploy the contract you need to interact with.

  2. Perform a series of transactions to get the contract into the relevant state.

  3. Run the test.

  4. Do this all over again for each test.

This is annoying, slow, and brittle. You might have to update a bunch of tests if the behavior of the other contract ever changes. Developers usually end up using tricks like state snapshots and complex test fixtures to get around this problem. Instead, you can use smock:

  1. Create a fake.

  2. Make your fake return the value you want it to return.

  3. Run the test.

Using fakes

Initialization

Initialize with a contract name
const myFake = await smock.fake('MyContract');
Initialize with a contract ABI
const myFake = await smock.fake([ { ... } ]);
Initialize with a contract factory
const myContractFactory = await hre.ethers.getContractFactory('MyContract');
const myFake = await smock.fake(myContractFactory);
Initialize with a contract instance
const myContractFactory = await hre.ethers.getContractFactory('MyContract');
const myContract = await myContractFactory.deploy();
const myFake = await smock.fake(myContract);
Take full advantage of typescript and typechain
const myFake = await smock.fake<MyContract>('MyContract');
Options
await smock.fake('MyContract', { ... }); // how to use

// options
{
  address?: string; // initialize fake at a specific address
  provider?: Provider; // initialize fake with a custom provider
}

Signing transactions

Every fake comes with a wallet property in order to make easy to sign transactions

myContract.connect(myFake.wallet).doSomething();

Making a function return

Returning with the default value
myFake.myFunction.returns();
Returning a fixed value
myFake.myFunction.returns(42);
Returning a struct
myFake.getStruct.returns({
  valueA: 1234,
  valueB: false,
});
Returning an array
myFake.myFunctionArray.returns([1, 2, 3]);
Returning a dynamic value
myFake.myFunction.returns(() => {
  if (Math.random() < 0.5) {
    return 0;
  } else {
    return 1;
  }
});
Returning a value based on arguments
myFake.myFunction.whenCalledWith(123).returns(456);

await myFake.myFunction(123); // returns 456
Returning a value with custom logic
myFake.getDynamicInput.returns(arg1 => arg1 * 10);

await myFake.getDynamicInput(123); // returns 1230
Returning at a specific call count
myFake.myFunction.returnsAtCall(0, 5678);
myFake.myFunction.returnsAtCall(1, 1234);

await myFake.myFunction(); // returns 5678
await myFake.myFunction(); // returns 1234

Making a function revert

Reverting with no data
myFake.myFunction.reverts();
Reverting with a string message
myFake.myFunction.reverts('Something went wrong');
Reverting with bytes data
myFake.myFunction.reverts('0x12341234');
Reverting at a specific call count
myFake.myFunction.returns(1234);
myFake.myFunction.revertsAtCall(1, 'Something went wrong');

await myFake.myFunction(); // returns 1234
await myFake.myFunction(); // reverts with 'Something went wrong'
await myFake.myFunction(); // returns 1234
Reverting based on arguments
myFake.myFunction.returns(1);
myFake.myFunction.whenCalledWith(123).reverts('Something went wrong');

await myFake.myFunction(); // returns 1
await myFake.myFunction(123); // reverts with 'Something went wrong'

Resetting function behavior

Resetting a function to original behavior
myFake.myFunction().reverts();

await myFake.myFunction(); // reverts

myFake.myFunction.reset(); // resets behavior for all inputs of the function

await myFake.myFunction(); // returns 0

Asserting call count

Any number of calls
expect(myFake.myFunction).to.have.been.called;
Called once
expect(myFake.myFunction).to.have.been.calledOnce;
Called twice
expect(myFake.myFunction).to.have.been.calledTwice;
Called three times
expect(myFake.myFunction).to.have.been.calledThrice;
Called N times
expect(myFake.myFunction).to.have.callCount(123);

Asserting call arguments or value

Called with specific arguments
expect(myFake.myFunction).to.have.been.calledWith(123, true, 'abcd');
Called with struct arguments
expect(myFake.myFunction).to.have.been.calledWith({
  myData: [1, 2, 3, 4],
  myNestedStruct: {
    otherValue: 5678
  }
});
Called at a specific call index with arguments
expect(myFake.myFunction.atCall(2)).to.have.been.calledWith(1234, false);
Called once with specific arguments
expect(myFake.myFunction).to.have.been.calledOnceWith(1234, false);
Called with an specific call value
expect(myFake.myFunction).to.have.been.calledWithValue(1234);

Asserting call order

Called before other function
expect(myFake.myFunction).to.have.been.calledBefore(myFake.myOtherFunction);
Called after other function
expect(myFake.myFunction).to.have.been.calledAfter(myFake.myOtherFunction);
Called immediately before other function
expect(myFake.myFunction).to.have.been.calledImmediatelyBefore(myFake.myOtherFunction);
Called immediately after other function
expect(myFake.myFunction).to.have.been.calledImmediatelyAfter(myFake.myOtherFunction);

Querying call arguments

Getting arguments at a specific call index
expect(myFake.myFunction.getCall(0).args[0]).to.be.gt(50);
Getting call value at a specific call index
expect(myFake.myFunction.getCall(0).value).to.eq(1);

Manipulating fallback functions

Modifying the fallback function
myFake.fallback.returns();
Modifying the receive function
myFake.receive.returns();

Delegated calls

Calls to a contract function via delegated calls do behave the same as a regular call, so you can enforce a return value, assert the calls details, etc… In addition, you also have custom assertions for delegated calls.

Assert delegated caller
expect(myFake.myFunction).to.be.delegatedFrom(myProxy.address);

Mocks

What are mocks?

Mocks are extensions to smart contracts that have all of the functionality of a fake with some extra goodies. Behind every mock is a real smart contract (with actual Solidity code!) of your choosing. You can modify the behavior of functions like a fake, or you can leave the functions alone and calls will pass through to your actual contract code. And, with a little bit of smock magic, you can even modify the value of variables within your contract! 🥳

When should I use a mock?

Generally speaking, mocks are more advanced versions of fakes. Mocks are most effectively used when you need some behavior of a real smart contract but still want the ability to modify things on the fly.

One powerful feature of a mock is that you can modify the value of variables within the smart contract. You could, for example, use this feature to test the behavior of a function that changes behavior depending on the value of a variable.

Using mocks

Initialization

Initialize with a contract name
const myContractFactory = await smock.mock('MyContract');
const myContract = await myContractFactory.deploy(...);
Take full advantage of typescript and typechain
await smock.mock<MyContract__factory>('MyContract');
Options
await smock.mock('MyContract', { ... }); // how to use

// options
{
  provider?: Provider; // initialize mock with a custom provider
}

Using features of fakes

Mocks can use any feature available to fakes. See the documentation of fakes for more information.

Call through

Calls go through to contract by default
await myMock.add(10);
await myMock.count(); // returns 10

myMock.count.returns(1);
await myMock.count(); // returns 1

Manipulating variables

Warning

This is an experimental feature and it is subject to API changes in the near future

Setting the value of a variable
await myMock.setVariable('myVariableName', 1234);
Setting the value of a struct
await myMock.setVariable('myStruct', {
  valueA: 1234,
  valueB: true,
});
Setting the value of a mapping (won’t affect other keys)
await myMock.setVariable('myMapping', {
  myKey: 1234
});
Setting the value of a nested mapping
await myMock.setVariable('myMapping', {
  myChildMapping: {
    myKey: 1234
  }
});
Setting the value of multiple variables
await myMock.setVariables({
  myVariableName1: 123,
  myVariableName2: true,
  myStruct: {
    valueA: 1234,
    valueB: false,
  },
  myMapping: {
    [myKey]: 1234
  }
})

Getting the value of an internal variable

Warning

This is an experimental feature and it does not support multidimensional or packed arrays

const myUint256 = await myMock.getVariable('myUint256VariableName');
Getting the value of an internal mapping given the value’s key
const myMappingValue = await myMock.getVariable('myMappingVariableName', [mappingKey]);
Getting the value of an internal nested mapping given the value’s keys
const myMappingValue = await myMock.getVariable('myMappingVariableName', [mappingKeyA, mappingKeyB]);

Development

Code

Open an issue or a PR, we will try to see it asap.

Docs

In order to continue developing the docs, you will first need to install the needed dependencies locally by running:

yarn docs:install

Then you can run the sphinx autobuild to see your changes live:

yarn docs:watch

Migrating from v1 to v2

DeFi Wonderland and Optimism have decided to join forces with our shady-super-coder’s magic to launch a new and improved version of the mocking library you ❤️

We know the breaking changes on the API will make you do some leg work, but we promise it is going to be totally worth it!

Also, special thanks to the Optimism team for recognizing our work and allowing us to host the new library on our Github organization (this marks our first public release 🚀)

Smock V2 focuses mainly on:

  • API improvements

  • Call arguments expectations

  • Custom chai matchers

  • Type extensions with generics

  • Fakes and Mocks division

  • Documentation


Before upgrading

If using Typescript, we highly recommend using Typechain in order to take full advantage of the type extensions we provide. If you decide not to, you can still follow along by using the type Contract from ethers or any.

With Typechain:

import { FakeContract } from '@defi-wonderland/smock';
import { CookieEater } from '@typechained';

let cookieEater: FakeContract<CookieEater>; // will extend all of the CookieEater method types

Without Typechain:

import { FakeContract } from '@defi-wonderland/smock';
import { Contract } from 'ethers';

let cookieEater: FakeContract<Contract>; // will extend all of the CookieEater method types

Installation

Uninstall the old package

yarn remove @eth-optimism/smock

Install the new one

yarn add --dev @defi-wonderland/smock

New concepts

Instead of having Mock and Smod objects, now we use Fakes and Mocks.

  • Fakes are empty contracts that emulate a given interface.
    All of their functions can be watched and pre-programmed. When calling a function of a fake, by default, it will return the return type zero-state.
  • Mocks are deployed contract wrappers that have all of the fake’s functionality and even more.
    Because they are actually deployed contract, they can have actual logic inside that can be called through. And because they have a storage, internal variable values can be overwritten 🥳

API changes

Smockit initialization

Before:

import { ethers } from 'hardhat';
import { smockit } from '@eth-optimism/smock';

const myContractFactory = await ethers.getContractFactory('MyContract');
const myContract = await myContractFactory.deploy(...);
const myMockContract = await smockit(myContract);

After:

import { smock } from '@defi-wonderland/smock';
import { MyContract } from '@typechained';

const myFakeContract = await smock.fake<MyContract>('MyContract');

Returns

Before:

myMockContract.smocked.myFunction.will.return.with('Some return value!');

After:

myFakeContract.myFunction.returns('Some return value!');

Asserting call count

Before:

expect(myMockContract.smocked.myFunction.calls.length).to.equal(1);

After:

expect(myFakeContract.myFunction).to.be.calledOnce;

Asserting call data

Before:

expect(MyMockContract.smocked.myFunction.calls.length).to.equal(1);
expect(MyMockContract.smocked.myFunction.calls[0]).to.deep.equal(['Something', 123]);

After:

expect(myFakeContract.myFunction).to.be.calledOnceWith('Something', 123);

Reverting

Before:

myMockContract.smocked.myFunction.will.revert();
myMockContract.smocked.myOtherFunction.will.revert.with('Some error');

After:

myFakeContract.myFunction.reverts();
myFakeContract.myOtherFunction.reverts('Some error');

Creating a modifiable contract

Before:

import { ethers } from 'hardhat';
import { smoddit } from '@eth-optimism/smock';

const myModifiableContractFactory = await smoddit('MyContract');
const myModifiableContract = await MyModifiableContractFactory.deploy(...);

After:

import { MyContract } from '@typechained';
import { MockContract, MockContractFactory, smock } from '@defi-wonderland/smock';

const myMockContractFactory: MockContractFactory<MyContract> = await smock.mock('MyContract');
const myMockContract: MockContract<MyContract> = await myMockContractFactory.deploy(...);

Modifying a contract variable value

Before:

await myModifiableContract.smodify.put({
  _myInternalVariable: 1234
});

After:

await myMockContract.setVariable('_myInternalVariable', 1234);

And more…

Smock V2 contains plenty of new features, you can check them all out in the docs!