Testing upgradeable projects

When working with the OpenZeppelin SDK, you can test your contracts as you usually do. That is, you can manually deploy your logic contracts, and test them just like any other contract. However, when using the OpenZeppelin SDK, you are dealing with upgradeable instances. Of course, you could use the OpenZeppelin SDK at a lower level programmatically in your tests but this could be rather cumbersome.

Instead, you can use specifically designed OpenZeppelin SDK tools that automatically set up your entire project in your testing environment. This allows you to replicate the same set of contracts that manage your project for each test you run.

The @openzeppelin/cli package provides a TestHelper() function to retrieve your project structure from the .openzeppelin/project.json file and deploy everything to the current test network. All the contracts that you have registered via openzeppelin add, plus all the contracts provided by the Ethereum Packages you have linked, will be available. The returned project object (either a ProxyAdminProject or an AppProject) provides convenient methods for creating upgradeable instances of your contracts, which you can use within your tests. Let’s see how this would work in a simple project.

Setting up a sample project

The following section describes a succinct way in how a simple OpenZeppelin SDK project can be set up. If you already have a project set up, you may skip to the next section.

If you don’t understand what’s going on in this section, please refer to the Quickstart guides of the documentation, specifically the Deploying your first project, Upgrading your project and Linking to Ethereum Packages guides. These guides provide detailed explanations on how a basic OpenZeppelin SDK project works.

Create a new project by running:

mkdir my-project
cd my-project
npm init --yes
npm install @openzeppelin/cli @openzeppelin/upgrades @openzeppelin/contracts-ethereum-package truffle chai

Now, run:

npx openzeppelin init my-project

Let’s add a simple contract to the project, create the file contracts/Sample.sol:

pragma solidity ^0.5.0;

contract Sample {
  function greet() public pure returns (string memory) {
    return "A sample";
  }
}

Now, add your contract to your OpenZeppelin SDK project:

npx openzeppelin add Sample

And link your OpenZeppelin SDK project to the @openzeppelin/contracts-ethereum-package Ethereum Package:

npx openzeppelin link @openzeppelin/contracts-ethereum-package

Writing the test script

This test is written in ES5 Javascript. If you’d like to use ES6 syntax instead, make sure you set up babel in your project.

Now, let’s create the test file test/Sample.test.js:

const { TestHelper } = require('@openzeppelin/cli');
const { Contracts, ZWeb3 } = require('@openzeppelin/upgrades');

ZWeb3.initialize(web3.currentProvider);

const Sample = Contracts.getFromLocal('Sample');
const ERC20 = Contracts.getFromNodeModules('openzeppelin-contracts-ethereum-package', 'ERC20');

require('chai').should();

contract('Sample', function () {

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

  it('should create a proxy', async function () {
    const proxy = await this.project.createProxy(Sample);
    const result = await proxy.methods.greet().call();
    result.should.eq('A sample');
  })

  it('should create a proxy for the Ethereum Package', async function () {
    const proxy = await this.project.createProxy(ERC20, { contractName: 'StandaloneERC20', packageName: '@openzeppelin/contracts-ethereum-package' });
    const result = await proxy.methods.totalSupply().call();
    result.should.eq('0');
  })
})

Next, modify your package.json file to include the following script:

"test": "truffle test"

And run the test in your console with:

npm test

That’s it! Now, let’s look at what we just did in more detail.

Understanding the test script

We first require TestHelper from @openzeppelin/cli. This helper facilitates the creation of a project object that will set up the entire OpenZeppelin SDK project within a test environment.

const { TestHelper } = require('@openzeppelin/cli');

We are also requiring Contracts and ZWeb3 from @openzeppelin/upgrades. Contracts helps us retrieve compiled contract artifacts, while ZWeb3 is needed to set up our Web3 provider in the OpenZeppelin SDK.

const { Contracts, ZWeb3 } = require('@openzeppelin/upgrades');

ZWeb3.initialize(web3.currentProvider);

const Sample = Contracts.getFromLocal('Sample');
const ERC20 = Contracts.getFromNodeModules('@openzeppelin/contracts-ethereum-package', 'ERC20');

We then invoke TestHelper in the test, optionally including a set of options to be used when deploying the contracts (such as from, gas, and gasPrice):

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

And finally, we add the tests themselves. Notice how each test first creates a upgradeable instance for each contract:

const proxy = await this.project.createProxy(Sample);

The createProxy method of the project accepts an additional object parameter in which you can specify an initializer function with arguments, just as you would by using the regular openzeppelin create command from the CLI, but due to the simplicity of this example, it’s not necessary in our case. If you would need to pass parameters though, you would do so like this:

const proxy = await this.project.createProxy(Sample, {
  initMethod: 'initialize',
  initArgs: [42]
});

This is assuming our Sample contract had an initialize function with one uint256 parameter, which it doesn’t. The above code simply illustrates how you would create the upgradeable instance if it had an initialize function.

Continuing with our example, notice that the way we interact with the contracts is by using their methods object. This is because the OpenZeppelin SDK uses the web3.js 1.0 Contract interface:

const result = await proxy.methods.totalSupply().call();

This is how you should write tests for your OpenZeppelin SDK projects. The project object provided by TestHelper wraps all of the OpenZeppelin SDK programmatic interface in a way that is very convenient to use in tests. By running tests in this way, you make sure that you are testing your contracts with the exact set of conditions that they would have in production, after you deploy them with the OpenZeppelin SDK.

Calling initialize functions manually in your tests

Sometimes, there are situations where a contract has functions that have matching names, but different arities. Here’s an example of a TimedCrowdsale contract that inherits from Crowdsale which results in a contract that has two initialize functions with different arities:

contract TimedCrowdsale is Crowdsale {

  initialize(uint256 _openingTime, uint256 _closingTime) public initializer {
    Crowdsale.initialize(_rate, _wallet, _token);
  }
}

contract Crowdsale {

  initialize(uint256 _rate, address _wallet, ERC20 _token) public initializer {
    // does something
  }
}

This means that calls to contracts with more than one function named initialize, as is the case with some contracts from OpenZeppelin (e.g., TimedCrowdsale), may revert if you call initialize directly from Truffle. openzeppelin create handles this correctly as it encodes the parameters. However, for your unit tests you will need to call initialize manually.

As of version 5, Truffle has the ability to overcome the problem depicted above. That is, you can call functions with matching names that have different arities in Javascript by using the methods property of Truffle Contract.

To call TimedCrowdsale’s initialize function, use the following syntax:

timedCrowadsale.methods['initialize(uint256,uint256)'](openingTime, closingTime);

And to call Crowdsale’s initialize function,

timedCrowadsale.methods['initialize(uint256,address,address)'](rate, wallet, token);