-
Notifications
You must be signed in to change notification settings - Fork 162
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Jest Matchers for Smart Contract Development (#321)
* initial commit for jest matchers * make the simplest test work: toBeProperAddress * make tests for toBeProperAddress pass * refactor matcher to its own folder * rename properAddress to toBeProperAddress * implement toBeProperPrivateKey * implement toBeProperHex * add BigNumber jest matchers * implement toChangeBalance * implement toChangeBalances * rename tests to get CI to pass * implement toBeReverted * implement toBeRevertedWith * tighten up types * implement toEmit * remove union test * implement first rough implementation of toEmitWithArgs * implement calledOnContract and validators * implement calledOnContractWith * rename jest.d.ts to types.d.ts * update files dependent on types * rewritten matchers for events emitted * add jest timeout 10 sec * ensure message always returns a string
- Loading branch information
1 parent
6c093f4
commit a8ef989
Showing
38 changed files
with
4,014 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
const baseConfig = require("../.eslintrc.json"); | ||
|
||
module.exports = { | ||
...baseConfig, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
module.exports = { | ||
preset: 'ts-jest', | ||
testEnvironment: 'node', | ||
setupFilesAfterEnv: ['./test/setupJest.ts'] | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
{ | ||
"name": "@ethereum-waffle/jest", | ||
"description": "A sweet set of jest matchers for your blockchain testing needs.", | ||
"version": "3.0.0", | ||
"author": "Adrian Li <li.adrianmc@gmail.com>", | ||
"repository": "git@github.com:EthWorks/Waffle.git", | ||
"private": false, | ||
"license": "MIT", | ||
"keywords": [ | ||
"ethereum", | ||
"smart-contracts", | ||
"solidity", | ||
"testing", | ||
"javascript", | ||
"typescript", | ||
"jest", | ||
"library" | ||
], | ||
"homepage": "https://github.com/EthWorks/Waffle", | ||
"bugs": { | ||
"url": "https://github.com/EthWorks/Waffle/issues" | ||
}, | ||
"main": "dist/cjs/index.js", | ||
"module": "dist/esm/index.ts", | ||
"types": "dist/esm/index.d.ts", | ||
"scripts": { | ||
"prepublishOnly": "yarn build", | ||
"test": "jest", | ||
"lint": "eslint '{src,test}/**/*.ts'", | ||
"lint:fix": "eslint --fix '{src,test}/**/*.ts'", | ||
"build": "rimraf ./dist && yarn build:esm && yarn build:cjs", | ||
"build:esm": "tsc -p tsconfig.build.json --outDir dist/esm --module ES6", | ||
"build:cjs": "tsc -p tsconfig.build.json --outDir dist/cjs --declaration false" | ||
}, | ||
"engines": { | ||
"node": ">=10.0" | ||
}, | ||
"dependencies": { | ||
"@ethereum-waffle/provider": "^3.0.0", | ||
"ethers": "^5.0.0", | ||
"jest-diff": "^26.0.1" | ||
}, | ||
"devDependencies": { | ||
"@types/jest": "^26.0.0", | ||
"@typescript-eslint/eslint-plugin": "^2.30.0", | ||
"@typescript-eslint/parser": "^2.30.0", | ||
"eslint": "^6.8.0", | ||
"eslint-plugin-import": "^2.20.2", | ||
"jest": "^26.0.1", | ||
"rimraf": "^3.0.0", | ||
"ts-jest": "^26.1.0", | ||
"ts-node": "^8.9.1", | ||
"typescript": "^3.8.3" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import {toBeProperAddress} from './matchers/toBeProperAddress'; | ||
import {toBeProperPrivateKey} from './matchers/toBeProperPrivateKey'; | ||
import {toBeProperHex} from './matchers/toBeProperHex'; | ||
import {bigNumberMatchers} from './matchers/bigNumber'; | ||
import {toChangeBalance} from './matchers/toChangeBalance'; | ||
import {toChangeBalances} from './matchers/toChangeBalances'; | ||
import {toBeReverted} from './matchers/toBeReverted'; | ||
import {toBeRevertedWith} from './matchers/toBeRevertedWith'; | ||
import {toHaveEmitted} from './matchers/toHaveEmitted/toHaveEmitted'; | ||
import {toHaveEmittedWith} from './matchers/toHaveEmitted/toHaveEmittedWith'; | ||
import {toBeCalledOnContract} from './matchers/calledOnContract/calledOnContract'; | ||
import {toBeCalledOnContractWith} from './matchers/calledOnContract/calledOnContractWith'; | ||
|
||
export const waffleJest = { | ||
// misc matchers | ||
toBeProperAddress, | ||
toBeProperPrivateKey, | ||
toBeProperHex, | ||
|
||
// BigNumber matchers | ||
...bigNumberMatchers, | ||
|
||
// balance matchers | ||
toChangeBalance, | ||
toChangeBalances, | ||
|
||
// revert matchers | ||
toBeReverted, | ||
toBeRevertedWith, | ||
|
||
// emit matchers | ||
toHaveEmitted, | ||
toHaveEmittedWith, | ||
|
||
// calledOnContract matchers | ||
toBeCalledOnContract, | ||
toBeCalledOnContractWith | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import {BigNumber} from 'ethers'; | ||
import {Numberish} from '../types'; | ||
|
||
// NOTE: Jest does not currently support overriding matchers while calling | ||
// original implementation, therefore we have to name our matchers something | ||
// different: https://github.com/facebook/jest/issues/6243 | ||
|
||
export const bigNumberMatchers = { | ||
toEqBN(received: Numberish, value: Numberish) { | ||
const pass = BigNumber.from(received).eq(value); | ||
return pass | ||
? { | ||
pass: true, | ||
message: () => `Expected "${received}" NOT to be equal ${value}` | ||
} | ||
: { | ||
pass: false, | ||
message: () => `Expected "${received}" to be equal ${value}` | ||
}; | ||
}, | ||
toBeGtBN(received: Numberish, value: Numberish) { | ||
const pass = BigNumber.from(received).gt(value); | ||
return pass | ||
? { | ||
pass: true, | ||
message: () => | ||
`Expected "${received}" NOT to be greater than ${value}` | ||
} | ||
: { | ||
pass: false, | ||
message: () => `Expected "${received}" to be greater than ${value}` | ||
}; | ||
}, | ||
toBeLtBN(received: Numberish, value: Numberish) { | ||
const pass = BigNumber.from(received).lt(value); | ||
return pass | ||
? { | ||
pass: true, | ||
message: () => `Expected "${received}" NOT to be less than ${value}` | ||
} | ||
: { | ||
pass: false, | ||
message: () => `Expected "${received}" to be less than ${value}` | ||
}; | ||
}, | ||
toBeGteBN(received: Numberish, value: Numberish) { | ||
const pass = BigNumber.from(received).gte(value); | ||
return pass | ||
? { | ||
pass: true, | ||
message: () => | ||
`Expected "${received}" NOT to be greater than or equal ${value}` | ||
} | ||
: { | ||
pass: false, | ||
message: () => | ||
`Expected "${received}" to be greater than or equal ${value}` | ||
}; | ||
}, | ||
toBeLteBN(received: Numberish, value: Numberish) { | ||
const pass = BigNumber.from(received).lte(value); | ||
return pass | ||
? { | ||
pass: true, | ||
message: () => | ||
`Expected "${received}" NOT to be less than or equal ${value}` | ||
} | ||
: { | ||
pass: false, | ||
message: () => | ||
`Expected "${received}" to be less than or equal ${value}` | ||
}; | ||
} | ||
}; |
32 changes: 32 additions & 0 deletions
32
waffle-jest/src/matchers/calledOnContract/calledOnContract.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import {Contract} from 'ethers'; | ||
import { | ||
validateContract, | ||
validateMockProvider, | ||
validateFnName | ||
} from './calledOnContractValidators'; | ||
|
||
export function toBeCalledOnContract(fnName: string, contract: Contract) { | ||
validateContract(contract); | ||
validateMockProvider(contract.provider); | ||
if (fnName !== undefined) { | ||
validateFnName(fnName, contract); | ||
} | ||
|
||
const fnSighash = contract.interface.getSighash(fnName); | ||
const {callHistory} = contract.provider; | ||
|
||
const pass = callHistory.some( | ||
(call) => | ||
call.address === contract.address && call.data.startsWith(fnSighash) | ||
); | ||
|
||
return pass | ||
? { | ||
pass: true, | ||
message: () => 'Expected contract function NOT to be called' | ||
} | ||
: { | ||
pass: false, | ||
message: () => 'Expected contract function to be called' | ||
}; | ||
} |
44 changes: 44 additions & 0 deletions
44
waffle-jest/src/matchers/calledOnContract/calledOnContractValidators.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import {Contract} from 'ethers'; | ||
import {MockProvider} from '@ethereum-waffle/provider'; | ||
import {ProviderWithHistoryExpected} from './error'; | ||
import {ensure} from './utils'; | ||
|
||
export function validateContract(contract: any): asserts contract is Contract { | ||
ensure( | ||
contract instanceof Contract, | ||
TypeError, | ||
'argument must be a contract' | ||
); | ||
} | ||
|
||
export function validateMockProvider( | ||
provider: any | ||
): asserts provider is MockProvider { | ||
ensure( | ||
!!provider.callHistory && provider.callHistory instanceof Array, | ||
ProviderWithHistoryExpected | ||
); | ||
} | ||
|
||
export function validateFnName( | ||
fnName: any, | ||
contract: Contract | ||
): asserts fnName is string { | ||
ensure( | ||
typeof fnName === 'string', | ||
TypeError, | ||
'function name must be a string' | ||
); | ||
function isFunction(name: string) { | ||
try { | ||
return !!contract.interface.getFunction(name); | ||
} catch (e) { | ||
return false; | ||
} | ||
} | ||
ensure( | ||
isFunction(fnName), | ||
TypeError, | ||
'function must exist in provided contract' | ||
); | ||
} |
32 changes: 32 additions & 0 deletions
32
waffle-jest/src/matchers/calledOnContract/calledOnContractWith.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import {Contract} from 'ethers'; | ||
import { | ||
validateContract, | ||
validateMockProvider, | ||
validateFnName | ||
} from './calledOnContractValidators'; | ||
|
||
export function toBeCalledOnContractWith( | ||
fnName: string, | ||
contract: Contract, | ||
parameters: any[] | ||
) { | ||
validateContract(contract); | ||
validateMockProvider(contract.provider); | ||
validateFnName(fnName, contract); | ||
|
||
const funCallData = contract.interface.encodeFunctionData(fnName, parameters); | ||
const {callHistory} = contract.provider; | ||
const pass = callHistory.some( | ||
(call) => call.address === contract.address && call.data === funCallData | ||
); | ||
|
||
return pass | ||
? { | ||
pass: true, | ||
message: () => 'Expected contract function with parameters NOT to be called' | ||
} | ||
: { | ||
pass: false, | ||
message: () => 'Expected contract function with parameters to be called' | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export class ProviderWithHistoryExpected extends Error { | ||
constructor() { | ||
super( | ||
'calledOnContract matcher requires provider that support call history' | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
interface ErrorConstructor<T extends any[]> { | ||
new (...args: T): Error; | ||
} | ||
|
||
export function ensure<T extends any[]>( | ||
condition: boolean, | ||
ErrorToThrow: ErrorConstructor<T>, | ||
...errorArgs: T | ||
): asserts condition { | ||
if (!condition) { | ||
throw new ErrorToThrow(...errorArgs); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
export function toBeProperAddress(received: string) { | ||
const pass = /^0x[0-9-a-fA-F]{40}$/.test(received); | ||
return pass | ||
? { | ||
pass: true, | ||
message: () => `Expected "${received}" not to be a proper address` | ||
} | ||
: { | ||
pass: false, | ||
message: () => `Expected "${received}" to be a proper address` | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
export function toBeProperHex(received: string, length: number) { | ||
const regexp = new RegExp(`^0x[0-9-a-fA-F]{${length}}$`); | ||
const pass = regexp.test(received); | ||
return pass | ||
? { | ||
pass: true, | ||
message: () => `Expected "${received}" not to be a proper hex of length ${length}, but it was` | ||
} | ||
: { | ||
pass: false, | ||
message: () => `Expected "${received}" to be a proper hex of length ${length}` | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
export function toBeProperPrivateKey(received: string) { | ||
const pass = /^0x[0-9-a-fA-F]{64}$/.test(received); | ||
return pass | ||
? { | ||
pass: true, | ||
message: () => `Expected "${received}" not to be a proper private key` | ||
} | ||
: { | ||
pass: false, | ||
message: () => `Expected "${received}" to be a proper private key` | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
export async function toBeReverted(promise: Promise<any>) { | ||
try { | ||
await promise; | ||
return { | ||
pass: false, | ||
message: () => 'Expected transaction to be reverted' | ||
}; | ||
} catch (error) { | ||
const message = | ||
error instanceof Object && 'message' in error ? error.message : JSON.stringify(error); | ||
|
||
const isReverted = message.search('revert') >= 0; | ||
const isThrown = message.search('invalid opcode') >= 0; | ||
const isError = message.search('code=') >= 0; | ||
|
||
const pass = isReverted || isThrown || isError; | ||
if (pass) { | ||
return { | ||
pass: true, | ||
message: () => 'Expected transaction NOT to be reverted' | ||
}; | ||
} else { | ||
return { | ||
pass: false, | ||
message: () => | ||
`Expected transaction to be reverted, but other exception was thrown: ${error}` | ||
}; | ||
} | ||
} | ||
} |
Oops, something went wrong.