Giter VIP home page Giter VIP logo

mock-contract's Introduction

Build Status

MockContract

Simple Solidity contract to mock dependencies in truffle tests. It enables you to

  • Make dependent contracts return predefined values for different methods and arguments
  • Simulate exceptions such as revert and outOfGas
  • Assert on how often a dependency is called

MockContract allows for all of that without having to write a separate test contract each time.

Usage in your project

Install module from npm

npm i -D @gnosis.pm/mock-contract

Enable compilation of MockContract

  • Add a new Imports.sol to the contracts folder the project
  • Copy the following code into Imports.sol:
pragma solidity ^0.6.0;

// We import the contract so truffle compiles it, and we have the ABI 
// available when working from truffle console.
import "@gnosis.pm/mock-contract/contracts/MockContract.sol";
  • Use in javascript unit test:
const MockContract = artifacts.require("./MockContract.sol")

// Instantiate mock and make it return true for any invocation
const mock = await MockContract.new()
await mock.givenAnyReturnBool(true)

// instantiate "object under test" with mocked contract.
const contractToTest = await ComplextContract.new(mock.address)

Step by Step Example

Let's assume we want to test the following smart contract, which implements a simple bidding procedure:

pragma solidity ^0.6.0;
import "./Token.sol";

/**
 * Contract that stores the highest bidder while securing payment from a given ERC20 Token.
 * Upon each bid the cost of bidding is incremented.
 */
contract SimpleAuction {
  address public captor; // Address of the highest bidder
  
  Token private token;
  uint256 private cost;

  constructor(Token _token) public {
    token = _token;
  }

  function bid() public {
    if (token.transferFrom(msg.sender, this, cost + 1)) {
      require(token.transfer(captor, cost), "Refund failed");
      captor = msg.sender;
      cost += 1;
    }
  }
}

If we were to write unit tests for this class, we would have to provide an implementation of an ERC20 token contract. There are commonly two ways to deal with that:

  1. Use the real ERC20 token contract and configure it in a way that it will work for the test (e.g. call token.approve before bidding)
  2. Implement a fake Token contract that instead of real logic contains dummy implementations (e.g. return true for everything)

The problem with 1) is that the logic required to make our dependency behave in the intended way can be very complex and incurs additional maintenance work. It also doesn't isolate our tests - instead of only testing the unit under test it is testing the integration of multiple components.

Solution 2) requires writing a Fake contract for each dependency. This takes time and pollutes the repository and migration files with a lot of non-production code.

Mocking General Interactions

MockContract can act as a generic fake object for any type of contract. We can tell our mock what it should return upon certain invocations. For the example above, assume we want to write a test case where ERC20 transfers work just fine:

const MockContract = artifacts.require("./MockContract.sol")
const SimpleAuction = artifacts.require("./SimpleAuction.sol")
...
it('updates the captor', async () => {
  const mock = await MockContract.new()
  const auction = await SimpleAuction.new(mock.address)

  const trueEncoded = web3.eth.abi.encodeParameter("bool", true)
  await mock.givenAnyReturnBool(trueEncoded)
  await auction.bid({from: accounts[0]})
  
  assert.equal(accounts[0], await auction.captor.call())
})

In particular await mock.givenAnyReturnBool(true) will make it so that mock returns true on any method invocation.

A plain mock without any expectations will return nullish values by default (e.g. false for bool, 0 for uint, etc).

There are convenience methods for other types such as givenAnyReturnAddress or givenAnyReturnUint. The full mock interface can be found here.

Mocking Methods Individually

Now let's assume we want to test that the bid gets reverted if token.transfer succeeds but token.transferFrom fails:

const Token = artifacts.require("./Token.sol")
...
it('should fail if we fail to refund', async () => {
  const mock = await MockContract.new()
  const auction = await SimpleAuction.new(mock.address)
  const token = await Token.new();

  const transferFrom = token.contract.methods.transferFrom(0, 0, 0).encodeABI() // arguments don't matter
  const transfer = token.contract.methods.transfer(0,0).encodeABI() // arguments don't matter

  await mock.givenMethodReturnBool(transferFrom, true)
  await mock.givenMethodReturnBool(transfer, false)

  try {
    await auction.bid({from: accounts[1]})
    assert.fail("Should have reverted")
  } catch (e) {}
})

Different methods have different ABI encodings. mock.givenMethodReturnBool(bytes, boolean) takes the ABI encoded methodId as a first parameter and will only replace behavior for this method. There are two ways to construct the methodId. We recommend using the encodeABI call on the original contract's ABI:

// Arguments do not matter, mock will only extract methodId
const transferFrom = token.contract.transferFrom(0, 0, 0).encodeABI()

We could also create it manually using e.g.:

const transferFrom = web3.sha3("transferFrom(address,address,uint256)").slice(0,10) // first 4 bytes

However, the latter approach is not type-safe and can lead to unexpected test behavior if the ABI on the original contract changes. The first approach would give a much more descriptive compilation error in that case.

Again there are convenience functions for other return types (e.g. givenMethoReturnUint).

Mocked methods will take priority over mocks using any.

Mocking Methods & Arguments

We can also specify different behaviors for when the same method is called with different arguments:

it('Keeps the old bidder if the new bidder fails to transfer', async () => {
  ...
  const transferFromA = token.contract.methods.transferFrom(accounts[0], auction.address, 1).encodeABI()
  const transferFromB = token.contract.methods.transferFrom(accounts[1], auction.address, 2).encodeABI()

  await mock.givenCalldataReturnBool(transferFromA, true)
  await mock.givenCalldataReturnBool(transferFromB, false)

  await auction.bid({from: accounts[0]})
  await auction.bid({from: accounts[1]})
      
  assert.equal(accounts[0], await auction.captor.call())
})

This time we need to provide the full calldata. We can easily use the original contract's ABI encodeABIcall to generate it. Again, convenience functions for other return types exist (e.g. givenMethoReturnUint).

Mocked calls with exact calldata will takes priority over method mocks and any mocks.

Simulating Failure

We can also simulate EVM exceptions using MockContract. All methods are available for any, method and calldata specific calls:

// Revert upon any invocation on mock without a specific message
await mock.givenAnyRevert()

// Revert upon any invocation of `methodId` with the specific message
await mock.givenMethodRevertWithMessage(methodId, "Some specific message")

// Run out of gas, if mock is called with `calldata`
await mock.givenCalldataRunOutOfGas(calldata)

Inspect Invocations

It can sometime be useful to see how often a dependency has been called during a test-case. E.g. we might want to assert that transfer is not called if transferFrom failed in the first place:

it('only does the second transfer if the first transfer succeed', async () => {
  ...
  await mock.givenAnyReturnBool(false)
  await auction.bid()

  const transfer = token.contract.methods.transfer(0,0).encodeABI()
  
  const invocationCount = await mock.invocationCountForMethod.call(transfer)
  assert.equal(0, invocationCount)
})

We can inspect the total invocation of mock for all methods combined using await invocationCount.call() and for individual arguments using await invocationCountForCalldata.call(calldata).

Resetting mock

We can override existing behavior throughout the lifetime of the test:

await mock.givenAnyReturnBool(true) // will return true from now on
await auction.bid()
await mock.givenAnyReturnBool(false) // will return false from now on
await auction.bid()

Note that previously specified method and calldata based behavior will be unaffected by overriding any mock behavior.

To completely reset all behavior and clear invocation counts, we can call:

await mock.reset()

Complex return types

If the methods for returning the most commonly used types are not enough, we can manually ABI encode our responses with arbitrary solidity types:

const hello_world = web3.eth.abi.encodeParameter("string", 'Hello World!')
await mock.givenAnyReturn(hello_world);

This is a work in progress and feedback is highly appreciated. Please open an issue on GitHub and/or submit a pull request.

mock-contract's People

Contributors

denisgranha avatar fleupold avatar fvictorio avatar g-r-a-n-t avatar josojo avatar leckylao avatar rmeissner avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

mock-contract's Issues

Can't invoke mocked view functions more than 2 times in a single transaction

After upgrading to Sol 0.5.0, tests making multiple calls to view functions in a single transaction began failing. I brought this up to @fleupold who determined that it's a gas issue with updateInvocationCount

Turns out that it's a gas issue. https://github.com/gnosis/mock-contract/blob/master/contracts/MockContract.sol#L374 get's forwarded all but 1/64th of the gas and since it throws an exception when called in a view's function context that gas is used. This means there is only very little gas left for the remaining code. Having three of those reverts in a row seems to exceeds the gas limit provided by truffle. The fix would be to change the .call(abi.encodeWithSignature("update... to .call.gas(100000)(abi...

I'll be opening a PR for this shortly with a test that was previously failing.

Mock return tuple with arrays

I am trying to set up a mock return tuple. Is it possible to do something like:

    const finalizePriceEpoch = ftsoInterface.contract.methods.finalizePriceEpoch(0, true);
    const finalizePriceEpochReturn = web3.eth.abi.encodeParameters(
      ["address[]", "uint64[]", "uint256"], 
      [[], [], "0"]);
    await mockFtso.givenMethodReturn(finalizePriceEpoch, finalizePriceEpochReturn);

With the above, I get this error when calling givenMethodReturn:

  1) Contract: RewardManager.sol; test/unit/implementations/RewardManager.js; Reward manager unit tests
       Should finalize price epoch for winning ftso with no reward recipients:
     Error: invalid arrayify value (argument="value", value="[object Object]", code=INVALID_ARGUMENT, version=bytes/5.0.11)

Returning array of values

Hi, I am trying to simulate a call to UniswapV2Router02.swapExactETHForTokens() (link)

That method returns a uint[], so I tried mocking that using: abi.rawEncode([ "uint[]" ], [ [1] ])

Here is the complete method mock definition:

await uniswapV2Router02.givenMethodReturn(
      uniswapEncorder.encodeRouterETHForTokens(),
      abi.rawEncode([ "uint[]" ], [ [1] ])
    );

Is it possible to encode an array of values?

Thanks!

Wierd behaviour with encodeABI

I am using your library to test a smart contract that requires another one. I am also using npx hardhat test test/simpleTest.ts for testing. I am not sure if this is a bug or just me doing something I shouldn't. Here is my code for test/simpleTest.ts:

import { MockContractInstance, PChainStakeMirrorVerifierInstance, PChainStakeMirrorVerifierMockInstance } from "../../../../typechain-truffle";
import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { BN } from "bn.js";
import { ethers } from 'hardhat';

import { abi } from "../artifacts/contracts/Random.sol/Random.json"

import { RandomContract, RouletteContract, RouletteInstance } from '../typechain-types/';
import { RandomInstance } from "../typechain-types/contracts/Random";

const MockContract = artifacts.require("MockContract");
const Roulette: RouletteContract = artifacts.require('Roulette');
const Random: RandomInstance = artifacts.require('Random');

function bn(n: any) {
    return new BN(n.toString());
}

const IRandom = new web3.eth.Contract(
    abi as any
)

describe('Roulette', async () => {
    let roulette: RouletteInstance
    let owner: SignerWithAddress
    let randomMock: MockContractInstance

    beforeEach(async () => {

        
        randomMock = await MockContract.new();
        const methodGetRandom = IRandom.methods.getRandom(25).encodeABI()

        // await ethers.getSigners();
        [owner] = await ethers.getSigners();

        randomMock.givenCalldataReturn(
            methodGetRandom, 
            web3.eth.abi.encodeParameters(["uint256", "bool"], [12345 , true])
        )

        console.log(owner);
    })
})

This code produces TypeError: Cannot create property 'undefined' on string '0xcd4b69140000000000000000000000000000000000000000000000000000000000000019'. If I uncomment // await ethers.getSigners();, it works.

If I move randomMock... above getting owner, then owner is undefined. Again uncommenting that await ... line makes it work again.

I hope either I am doing something wrong or it helps you improve your library. If you need more information I can provide it.

Make it possible that the mock can call other contracts

E.g. add something similar to

function exec(address payable to, uint256 value, bytes calldata data) external {
    bool success;
    bytes memory response;
    (success,response) = to.call{value: value}(data);
    if(!success) {
        assembly {
            revert(add(response, 0x20), mload(response))
        }
    }
}

Partially matching call arguments

Background
The Gnosis MockContract is a smart contract enabling developers to quickly mock contract interactions for unit tests.
It enables developers to

  • Make dependent contracts return predefined values for different methods and arguments
  • Simulate exceptions such as revert and outOfGas
  • Assert on how often a dependency is called

The MockContract facilitates these features without requiring any separate test contracts.
Check out the repo: https://github.com/gnosis/mock-contract

Task
Extend the functionality of the MockContract to partially matching arguments.

Acceptance criteria

  • Develop a framework allowing developers to define partially matching calldata for specific methods. 
Defining partially matched call data should look similar to:
const particallyMatchedCalldata = contract.methods.methodName(
   accounts[0], ANY, ..., ANY
   ).encodeABI()

Here, ANY would be some sort of constant defined in the smart contract.

  • Write the logic for the MockContract to expose predefined returns to the partially matched calldata. 
It is expected that the following functions for partially matched calldata are provided:
	function givenPartialCalldataReturn(bytes calldata call, bytes calldata response) external;
	function givenPartialCalldataReturnBool(bytes calldata call, bool response) external;
	function givenPartialCalldataReturnUint(bytes calldata call, uint response) external;
	function givenPartialCalldataReturnAddress(bytes calldata call, address response) external;

	function givenPartialCalldataRevert(bytes calldata call) external;
	function givenPartialCalldataRevertWithMessage(bytes calldata call, string calldata message) external;
	function givenPartialCalldataRunOutOfGas(bytes calldata call) external;

They should enable the same functionality on partially matching calldata as the current respective functions with the same function names, but without the word "Partial".

  • Make sure the invocation count functions are still returning the expected numbers
:
       function invocationCount() external returns (uint);
       function invocationCountForMethod(bytes calldata method) external returns (uint);
       function invocationCountForCalldata(bytes calldata call) external returns (uint);
  • Current naming conventions need to be preseved
  • All functionality must have 100 % line and branch coverage
  • The solidity style guide must be considered and code must be linted

Payout:
0.5 ETH
Additional GNO can be tipped according to the level of technical implementation

Error: Could not find artifacts for .\MockContract.sol from any sources

I'm trying to use mock-contract and getting the following error:
image

Steps Iv'e taken:

  1. Added "@gnosis.pm/mock-contract": "^3.0.8", to my package.json (and of course ran npm i)
  2. Added a file called Imports.sol to my contracts folder with the following contents:
pragma solidity ^0.5.0;

// We import the contract so truffle compiles it, and we have the ABI
// available when working from truffle console.
import "@gnosis.pm/mock-contract/contracts/MockContract.sol";
  1. Since I'me using VS Code I had to add the following to my settings.json file:
    "solidity.packageDefaultDependenciesContractsDirectory": "",
    "solidity.packageDefaultDependenciesDirectory": "node_modules"
  1. Added const MockContract = artifacts.require("./MockContract.sol"); to the top of my test file.

@fleupold (or anyone else) are you able to tell me what the problem is?

By the way I just want to mention that I really appreciate that you built this. From what I can find, its the only smart contract mocking framework around. The only other alternativeseems to be just writing fake contracts which I really prefer not to do.

View functions revert when mocked

In Solidity 0.5.0 view functions are called via STATICCALL which disallows state modifications. MockContract's fallback function modifies state which causes mocked view functions to revert. Here's an example test that demonstrates this.

Update: When commenting out invocation recording the test passes.

Calldata with predicates

Background
The Gnosis MockContract is a smart contract enabling developers to quickly mock contract interactions for unit tests.
It enables developers to

  • Make dependent contracts return predefined values for different methods and arguments
  • Simulate exceptions such as revert and outOfGas
  • Assert on how often a dependency is called

The MockContract facilitates these features without requiring any separate test contracts. Check out the repo: https://github.com/gnosis/mock-contract

Task
Extend the functionality of the MockContract to mock contract interaction on calldata with predicate

s

Acceptance criteria:

  • Develop a framework allowing developers to define predicates on calldata for specific methods. 
The predicates should include GREATER_THAN with a definition similar to:
const calldata = contract.methods.foo(
GREATER_THAN(10)
).encodeABI()

and the predicate SMALL_THAN

const calldata = contract.methods.foo(
SMALLER_THAN(10)
).encodeABI()
  • Write the logic for the MockContract to expose predefined returns to calldata with predicates. 
Hence, it is expected that the following functions are provided for calldata with predicates:
	function givenPredicateCalldataReturn(bytes calldata call, bytes calldata response) external;
	function givenPredicateCalldataReturnBool(bytes calldata call, bool response) external;
	function givenPredicateCalldataReturnUint(bytes calldata call, uint response) external;
	function givenPredicateCalldataReturnAddress(bytes calldata call, address response) external;

	function givenPredicateCalldataRevert(bytes calldata call) external;
	function givenPredicateCalldataRevertWithMessage(bytes calldata call, string calldata message) external;
	function givenPredicateCalldataRunOutOfGas(bytes calldata call) external;

They should enable the same functionality on calldata with predicates as the current respective
functions with the same function names, but without the word "Predicate".

  • Make sure the invocation count functions are still returning the right numbers
:
       function invocationCount() external returns (uint);
       function invocationCountForMethod(bytes calldata method) external returns (uint);
       function invocationCountForCalldata(bytes calldata call) external returns (uint);
  • Current naming conventions need to be preserved
  • All functionality must have 100 % line and branch coverage
  • The solidity style guide must be considered and code must be linted

Payout:
0.75 ETH
Additional GNO can be tipped according to the level of technical implementation

Update NPM package

Love this mock contract and how carefully it's been designed, not to mention a well-written README :)

I noticed you guys have upgraded to solidity 0.6 but have not put the package on NPM. Any reason not to update it now?

Thanks!

Usage of .getData() no longer works

The example given in the README tutorial for Mocking Methods Individually

token.contract.transferFrom.getData(0, 0, 0)

doesn't seen to work.

The following seems to remedy this.

token.contract.methods.transferFrom(0, 0, 0).encodeABI();

taken from here

Version Info:
Truffle v5.0.8 (core: 5.0.8)
Solidity - ^0.5.0 (solc-js)
Node v11.12.0
Web3.js v1.0.0-beta.37

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.