Mocking upgradeable contracts in unit tests

Hello,

I am wondering how can I go about mocking upgradeable contracts? Am I able to use libraries such as https://github.com/gnosis/mock-contract still or does OZ provide some mock helper?

Thanks in advance

Hi @emobe,

There isn’t a mock helper as part of OpenZeppelin.
In OpenZeppelin Contracts, mock contracts are manually generated.

I haven’t used https://github.com/gnosis/mock-contract. If you are using a mock library, you may want to write unit tests that test just the logic contracts and have additional tests for testing as upgradeable contracts.

True, I can make the logic a separate contract. Would make it more SOLID.

Is there any reason not to use an upgradeable contract?

Hi @emobe,

I mean that for the one contract, you can create tests for the logic and tests for the upgradeability.

A very simple example is shown below (using OpenZeppelin Test Environment):

Counter.sol

pragma solidity ^0.5.0;

contract Counter {
  uint256 public value;

  function increase() public {
    value++;
  }
}

Counter.test.js

const { accounts, contract, web3 } = require('@openzeppelin/test-environment');

const { TestHelper } = require('@openzeppelin/cli');
const { Contracts, ZWeb3 } = require('@openzeppelin/upgrades');
const {
  BN,           // Big Number support
  constants,    // Common constants, like the zero address and largest integers
  expectEvent,  // Assertions for emitted events
  expectRevert, // Assertions for transactions that should fail
} = require('@openzeppelin/test-helpers');


ZWeb3.initialize(web3.currentProvider);

const Counter = Contracts.getFromLocal('Counter');

require('chai').should();

describe('Counter (upgradeable)', function () {

  beforeEach(async function () {
    this.project = await TestHelper();
    this.proxy = await this.project.createProxy(Counter);
  })

  it('should have a value', async function () {
    const result = await this.proxy.methods.value().call();
    result.should.be.bignumber.equal('0');
  })

  it('should increase value', async function () {
    await this.proxy.methods.increase().send();
    const result = await this.proxy.methods.value().call();
    result.should.be.bignumber.equal('1');
  })
})

Counter.logic.test.js

const { accounts, contract, web3 } = require('@openzeppelin/test-environment');

const {
  BN,           // Big Number support
  constants,    // Common constants, like the zero address and largest integers
  expectEvent,  // Assertions for emitted events
  expectRevert, // Assertions for transactions that should fail
} = require('@openzeppelin/test-helpers');

const Counter = contract.fromArtifact('Counter');

require('chai').should();

describe('Counter (logic)', function () {

  beforeEach(async function () {
    this.counter = await Counter.new();
  })

  it('should have a value', async function () {
    const result = await this.counter.value();
    result.should.be.bignumber.equal('0');
  })

  it('should increase value', async function () {
    await this.counter.increase();
    const result = await this.counter.value();
    result.should.be.bignumber.equal('1');
  })
})

Aside from the benefits of using OpenZeppelin SDK upgradeable contracts, some reasons to not create a contract as upgradeable:

  1. There is a gas overhead for using a Proxy.
  2. OpenZeppelin SDK contracts (post Istanbul) can't receive Ether from other contracts via transfer: OpenZeppelin upgradeable contracts affected by Istanbul hardfork
  3. Some entity needs to control upgrading a contract. Whether this is a single account, a multi-sig, a DAO or some other mechanism. (See Upgrades governance for using a multi-sig). This process for upgrading should be clearly explained to users under what circumstances it can occur.

Ah yes, makes sense now as it doesn’t need to be initialised in order to be tested. Thanks for your help

Hi @emobe,

We still need to call any initializers in logic and upgradeable tests, to ensure that the contract is initialized.