Context
While trying to set up a basic self-hosted unit testing environment (and CI) that tests this Chainlink VRF random number contract, I am experiencing slight difficulties in how to simulate any relevant blockchains/testnets locally.
For example, I found this repository that tests Chainlinks VRF. However, for default deployment it suggests/requires a free KOVAN_RPC_URL
e.g. from Infura's site and even for "local deployment" it suggests/requires a free MAINNET_RPC_URL
from e.g. Alchemy's site.
Attempt/baseline
I adopted a unit test environment from the waffle framework which is described as:
Filestructure
src____AmIRichAlready.sol
|____RandomNumberConsumer.sol
|
test____AmIRichAlready.test.ts
|____mocha.opts
package.json
tsconfig.json
waffle.json
yarn.lock
Filecontents
AmIRichAlready.sol
pragma solidity ^0.6.2;
interface IERC20 {
function balanceOf(address account) external view returns (uint256);
}
contract AmIRichAlready {
IERC20 private tokenContract;
uint public richness = 1000000 * 10 ** 18;
constructor (IERC20 _tokenContract) public {
tokenContract = _tokenContract;
}
function check() public view returns (bool) {
uint balance = tokenContract.balanceOf(msg.sender);
return balance > richness;
}
// IS THIS NEEDED???
function setRichness(uint256 _richness) public {
richness = _richness;
}
}
The RandomNumberConsumer.sol
filecontent is already on stackexange over here.
AmIRichAlready.test.ts
import {expect, use} from 'chai';
import {Contract, utils, Wallet} from 'ethers';
import {deployContract, deployMockContract, MockProvider, solidity} from 'ethereum-waffle';
import IERC20 from '../build/IERC20.json';
import AmIRichAlready from '../build/AmIRichAlready.json';
use(solidity);
describe('Am I Rich Already', () => {
let mockERC20: Contract;
let contract: Contract;
let vrfContract: Contract;
let wallet: Wallet;
beforeEach(async () => {
[wallet] = new MockProvider().getWallets();
mockERC20 = await deployMockContract(wallet, IERC20.abi);
contract = await deployContract(wallet, AmIRichAlready, [mockERC20.address]);
vrfContract = await deployContract(wallet, RandomNumberConsumer);
});
it('checks if contract called balanceOf with certain wallet on the ERC20 token', async () => {
await mockERC20.mock.balanceOf
.withArgs(wallet.address)
.returns(utils.parseEther('999999'));
await contract.check();
expect('balanceOf').to.be.calledOnContractWith(mockERC20, [wallet.address]);
});
it('returns false if the wallet has less than 1000000 coins', async () => {
await mockERC20.mock.balanceOf
.withArgs(wallet.address)
.returns(utils.parseEther('999999'));
expect(await contract.check()).to.be.equal(false);
});
it('returns true if the wallet has at least 1000000 coins', async () => {
await mockERC20.mock.balanceOf
.withArgs(wallet.address)
.returns(utils.parseEther('1000000'));
expect(await contract.check()).to.be.equal(false);
});
});
mocha.opts
-r ts-node/register/transpile-only
--timeout 50000
--no-warnings
test/**/*.test.{js,ts}
package.json
{
"name": "example-dynamic-mocking-and-testing-calls",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"test": "export NODE_ENV=test && mocha",
"build": "waffle",
"lint": "eslint '{src,test}/**/*.ts'",
"lint:fix": "eslint --fix '{src,test}/**/*.ts'"
},
"devDependencies": {
"@openzeppelin/contracts": "^4.3.1",
"@types/chai": "^4.2.3",
"@types/mocha": "^5.2.7",
"@typescript-eslint/eslint-plugin": "^2.30.0",
"@typescript-eslint/parser": "^2.30.0",
"chai": "^4.3.4",
"eslint": "^6.8.0",
"eslint-plugin-import": "^2.20.2",
"ethereum-waffle": "^3.4.0",
"ethers": "^5.0.17",
"mocha": "^7.2.0",
"ts-node": "^8.9.1",
"typescript": "^3.8.3"
}
}
tsconfig.json
{
"compilerOptions": {
"declaration": true,
"esModuleInterop": true,
"lib": [
"ES2018"
],
"module": "CommonJS",
"moduleResolution": "node",
"outDir": "dist",
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"target": "ES2018"
}
// custom test in vrfContract
it('Tests if a random number is returned', async () => {
expect(await vrfContract.getRandomNumber()).to.be.equal(7);
});
}
waffle.json
{
"compilerType": "solcjs",
"compilerVersion": "0.6.2",
"sourceDirectory": "./src",
"outputDirectory": "./build"
}
The yarn.lock
file content is a bit large, and it's auto-generated, so you can find it on the Waffle framework repository. Similarly, the package.json
can be found here, in the same repository.
Commands
One can also simply clone the repo with the specified filestructure here, and run the tests with the following commands:
git clone [email protected]:a-t-2/chainlink.git
git clone [email protected]:a-t-2/test_vrf3.git
cd test_vrf3
sudo apt install npm
npm install
npm audit fix
npm install --save-dev ethereum-waffle
npm install @openzeppelin/contracts -D
npm i chai -D
npm i mocha -D
rm -r build
npx waffle
npx mocha
npm test
Test Output
This will test the AmIRichAlready.sol
file and output:
Am I Rich Already
✓ checks if contract called balanceOf with certain wallet on the ERC20 token (249ms)
✓ returns false if the wallet has less than 1000000 coins (190ms)
✓ returns true if the wallet has at least 1000000 coins (159ms)
Tests if a random number is returned:
Error: cannot estimate gas; transaction may fail or may require manual gas limit (error={"name":"RuntimeError","results":{"0x0a0b028de6cf6e8446853a300061305501136cefa5f5eb3e96afd95dbd73dd92":{"error":"revert","program_counter":609,"return":"0x"}},"hashes":["0x0a0b028de6cf6e8446853a300061305501136cefa5f5eb3e96afd95dbd73dd92"],"message":"VM Exception while processing transaction: revert"}, tx={"data":"0xdbdff2c1","to":{},"from":"0x17ec8597ff92C3F44523bDc65BF0f1bE632917ff","gasPrice":{"type":"BigNumber","hex":"0x77359400"},"type":0,"nonce":{},"gasLimit":{},"chainId":{}}, code=UNPREDICTABLE_GAS_LIMIT, version=abstract-signer/5.4.1)
at Logger.makeError (node_modules/@ethersproject/logger/src.ts/index.ts:225:28)
at Logger.throwError (node_modules/@ethersproject/logger/src.ts/index.ts:237:20)
at /home/name/git/trucol/tested/new_test/test_vrf3/node_modules/@ethersproject/abstract-signer/src.ts/index.ts:301:31
at process._tickCallback (internal/process/next_tick.js:68:7)
3 passing (4s)
Question
Which set of files, file structure and commands do I need to automatically test whether the getRandomNumber()
contract returns an integer if sufficient "gas" is provided, and an error otherwise?