Skip to content

Commit

Permalink
Use Jest for tests (#107)
Browse files Browse the repository at this point in the history
* Switch to Jest. Start adding hardhat-config tests

* ESLint fixes

* Remove unused variable

* Adding more hardhat-config tests

* Prettier

* Finish testing hardhat-config

* Fix typo
  • Loading branch information
andreogle authored Oct 18, 2023
1 parent 1c299e1 commit ee80000
Show file tree
Hide file tree
Showing 8 changed files with 2,113 additions and 53 deletions.
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.env
*.env
coverage
dist/
node_modules/
.env
*.env
*.log
13 changes: 13 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* For a detailed explanation regarding each configuration property and type check, visit:
* https://jestjs.io/docs/configuration
*/
module.exports = {
collectCoverage: true,
coverageDirectory: 'coverage',
coverageProvider: 'v8',
modulePathIgnorePatterns: ['<rootDir>/.build', '<rootDir>/dist/', '<rootDir>/build/'],
preset: 'ts-jest',
restoreMocks: true,
testEnvironment: 'jest-environment-node',
};
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"prettier": "prettier --write \"./**/*.{ts,js,md,json}\"",
"providers:ping": "ts-node scripts/ping-providers.ts",
"providers:time": "ts-node scripts/calculate-average-block-times.ts",
"test": "node --test --loader ts-node/esm ./src/**/*.test.ts",
"test": "jest",
"validate": "yarn validate:chains",
"validate:chains": "ts-node scripts/validate-chains.ts"
},
Expand All @@ -35,15 +35,18 @@
"devDependencies": {
"@api3/promise-utils": "^0.4.0",
"@slack/web-api": "^6.9.0",
"@types/jest": "^29.5.5",
"@types/node": "^20.8.3",
"@types/prettier": "^3.0.0",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"eslint": "^8.51.0",
"eslint-plugin-import": "^2.28.1",
"husky": "^8.0.3",
"jest": "^29.7.0",
"prettier": "^3.0.3",
"rimraf": "^5.0.5",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
"typescript": "^5.2.2",
"viem": "^1.10.9"
Expand Down
234 changes: 234 additions & 0 deletions src/hardhat-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import { etherscan, etherscanApiKeyName, getEnvVariableNames, networkHttpRpcUrlName, networks } from './hardhat-config';
import { Chain } from './types';
import { CHAINS } from './generated/chains';
import { toUpperSnakeCase } from './utils/strings';

function getRandomChain(): Chain {
return CHAINS[Math.floor(Math.random() * CHAINS.length)]!;
}

const OLD_ENV = process.env;

beforeEach(() => {
jest.resetModules(); // Most important - it clears the cache
process.env = { ...OLD_ENV }; // Make a copy
});

afterAll(() => {
process.env = OLD_ENV; // Restore old environment
});

describe(getEnvVariableNames.name, () => {
test('returns an array with expected env variables', () => {
const apiKeyEnvNames = CHAINS.filter((chain) => chain.explorer?.api?.key?.required).map((chain) =>
etherscanApiKeyName(chain)
);
const networkRpcUrlNames = CHAINS.map((chain) => networkHttpRpcUrlName(chain));
const expected = ['MNEMONIC', ...apiKeyEnvNames, ...networkRpcUrlNames];
expect(getEnvVariableNames()).toEqual(expected);
});
});

describe(etherscanApiKeyName.name, () => {
test('returns the expected Etherscan API key name', () => {
const randomChain = getRandomChain();
const expected = `ETHERSCAN_API_KEY_${toUpperSnakeCase(randomChain!.alias)}`;
expect(etherscanApiKeyName(randomChain!)).toStrictEqual(expected);
});
});

describe(networkHttpRpcUrlName.name, () => {
test('returns the expected HTTP RPC URL name', () => {
const randomChain = getRandomChain();
const expected = `ETHERSCAN_API_KEY_${toUpperSnakeCase(randomChain!.alias)}`;
expect(etherscanApiKeyName(randomChain!)).toStrictEqual(expected);
});
});

describe(etherscan.name, () => {
beforeEach(() => {
expect((global as any).window).toBeUndefined();
});

afterEach(() => {
delete (global as any).window;
});

test('throws an error if called in a browser-like environment', () => {
(global as any).window = {};
expect(() => etherscan()).toThrow('Cannot be called outside of a Node.js environment');
});

describe('customChains', () => {
test('ignores chains without an explorer', () => {
const { customChains } = etherscan();
const ids = CHAINS.filter((c) => !c.explorer).map((c) => c.id);
customChains.forEach((c) => {
expect(ids).not.toContain(c.chainId);
});
});

test('ignores chains without an explorer API', () => {
const { customChains } = etherscan();
const ids = CHAINS.filter((c) => !!c.explorer && !c.explorer.api).map((c) => c.id);
customChains.forEach((c) => {
expect(ids).not.toContain(c.chainId);
});
});

test('ignores chains with a hardhat etherscan alias', () => {
const { customChains } = etherscan();
const chains = CHAINS.filter((c) => !!c.explorer && !!c.explorer.api);
const ids = chains.filter((c) => c.explorer.api!.key.hardhatEtherscanAlias).map((c) => c.id);

customChains.forEach((c) => {
expect(ids).not.toContain(c.chainId);
});
});

test('includes all other chains', () => {
const { customChains } = etherscan();
const chains = CHAINS.filter((c) => !!c.explorer && !!c.explorer.api);
const chainsWithoutAlias = chains.filter((c) => !c.explorer.api!.key.hardhatEtherscanAlias);

customChains.forEach((customChain) => {
const chain = chainsWithoutAlias.find((c) => c.id === customChain.chainId.toString())!;
expect(customChain).toEqual({
network: chain.alias,
chainId: Number(chain.id),
urls: {
apiURL: chain.explorer.api!.url,
browserURL: chain.explorer.browserUrl,
},
});
});
});
});

describe('apiKey', () => {
test('ignores chains without an explorer', () => {
const { apiKey } = etherscan();
const aliases = CHAINS.filter((c) => !c.explorer).map((c) => c.alias);
Object.keys(apiKey).forEach((key) => {
expect(aliases).not.toContain(key);
});
});

test('ignores chains without an explorer API', () => {
const { apiKey } = etherscan();
const aliases = CHAINS.filter((c) => !!c.explorer && !c.explorer.api).map((c) => c.alias);
Object.keys(apiKey).forEach((key) => {
expect(aliases).not.toContain(key);
});
});

test('sets the API key value to dummy value for chains with a hardhat alias', () => {
const chains = CHAINS.filter((c) => !!c.explorer && !!c.explorer.api);
const chainsWithAlias = chains.filter((c) => {
return (
!!c.explorer.api!.key.hardhatEtherscanAlias && // has a hardhatEtherscanAlias
!c.explorer.api!.key.required
); // but not required
});

const { apiKey } = etherscan();
chainsWithAlias.forEach((chain) => {
expect(apiKey[chain.explorer.api!.key.hardhatEtherscanAlias!]).toEqual('DUMMY_VALUE');
});
});

test('sets the API key value to not found for chains with a hardhat alias', () => {
const chains = CHAINS.filter((c) => !!c.explorer && !!c.explorer.api);
const chainsWithAlias = chains.filter((c) => {
return (
!!c.explorer.api!.key.hardhatEtherscanAlias && // has a hardhatEtherscanAlias
c.explorer.api!.key.required
); // and is required
});

const { apiKey } = etherscan();
chainsWithAlias.forEach((chain) => {
expect(apiKey[chain.explorer.api!.key.hardhatEtherscanAlias!]).toEqual('NOT_FOUND');
});
});

test('sets the API value to the env variable value for chains with a hardhat alias', () => {
const chains = CHAINS.filter((c) => !!c.explorer && !!c.explorer.api);
const chainsWithAlias = chains.filter((c) => {
return (
!!c.explorer.api!.key.hardhatEtherscanAlias && // has a hardhatEtherscanAlias
c.explorer.api!.key.required
); // and is required
});

chainsWithAlias.forEach((chain) => {
const envKey = etherscanApiKeyName(chain);
process.env[envKey] = `api-key-${chain.id}`;
});

// needs to be called AFTER env values are set
const { apiKey } = etherscan();

chainsWithAlias.forEach((chain) => {
expect(apiKey[chain.explorer.api!.key.hardhatEtherscanAlias!]).toEqual(`api-key-${chain.id}`);
});
});
});
});

describe(networks.name, () => {
beforeEach(() => {
expect((global as any).window).toBeUndefined();
});

afterEach(() => {
delete (global as any).window;
});

test('throws an error if called in a browser-like environment', () => {
(global as any).window = {};
expect(() => networks()).toThrow('Cannot be called outside of a Node.js environment');
});

test('builds a network object for each chain', () => {
const result = networks();
expect(Object.keys(result).length).toEqual(CHAINS.length);

CHAINS.forEach((chain) => {
expect(result[chain.alias]).toEqual({
accounts: { mnemonic: '' },
chainId: Number(chain.id),
url: chain.providerUrl,
});
});
});

test('sets the mnemonic using the MNEMONIC env variable if it exists', () => {
process.env.MNEMONIC = 'test test test test test test test test test test test junk';
const result = networks();
CHAINS.forEach((chain) => {
expect(result[chain.alias]).toEqual({
accounts: { mnemonic: 'test test test test test test test test test test test junk' },
chainId: Number(chain.id),
url: chain.providerUrl,
});
});
});

test('sets the provider URL using the chain alias env variable if it exists', () => {
CHAINS.forEach((chain) => {
const alias = toUpperSnakeCase(chain.alias);
process.env[`HARDHAT_HTTP_RPC_URL_${alias}`] = `https://${chain.id}.xyz`;
});

const result = networks();

CHAINS.forEach((chain) => {
expect(result[chain.alias]).toEqual({
accounts: { mnemonic: '' },
chainId: Number(chain.id),
url: `https://${chain.id}.xyz`,
});
});
});
});
2 changes: 1 addition & 1 deletion src/hardhat-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function networkHttpRpcUrlName(chain: Chain): string {
// https://hardhat.org/hardhat-runner/plugins/nomicfoundation-hardhat-verify#multiple-api-keys-and-alternative-block-explorers
export function etherscan(): HardhatEtherscanConfig {
if (typeof window !== 'undefined') {
throw new Error('Cannot be run outside of a Node.js environment');
throw new Error('Cannot be called outside of a Node.js environment');
}

return CHAINS.reduce(
Expand Down
20 changes: 9 additions & 11 deletions src/utils/strings.test.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,43 @@
import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { toUpperSnakeCase } from './strings';

describe('toUpperSnakeCase', () => {
describe(toUpperSnakeCase.name, () => {
test('converts simple words', () => {
const result = toUpperSnakeCase('hello world');
assert.equal(result, 'HELLO_WORLD');
expect(result).toEqual('HELLO_WORLD');
});

test('keeps numbers in the string', () => {
const result = toUpperSnakeCase('hello world 4');
assert.equal(result, 'HELLO_WORLD_4');
expect(result).toEqual('HELLO_WORLD_4');
});

test('trims leading and trailing whitespaces', () => {
const result = toUpperSnakeCase(' hello world ');
assert.equal(result, 'HELLO_WORLD');
expect(result).toEqual('HELLO_WORLD');
});

test('converts special characters to underscores', () => {
const result = toUpperSnakeCase('hello,world!');
assert.equal(result, 'HELLO_WORLD');
expect(result).toEqual('HELLO_WORLD');
});

test('converts special characters and spaces to underscores', () => {
const result = toUpperSnakeCase('hello, world!');
assert.equal(result, 'HELLO_WORLD');
expect(result).toEqual('HELLO_WORLD');
});

test('converts multiple spaces to single underscores', () => {
const result = toUpperSnakeCase('hello world');
assert.equal(result, 'HELLO_WORLD');
expect(result).toEqual('HELLO_WORLD');
});

test('returns an empty string when given an empty string', () => {
const result = toUpperSnakeCase('');
assert.equal(result, '');
expect(result).toEqual('');
});

test('converts mixed case strings', () => {
const result = toUpperSnakeCase('Hello World');
assert.equal(result, 'HELLO_WORLD');
expect(result).toEqual('HELLO_WORLD');
});
});
Loading

0 comments on commit ee80000

Please sign in to comment.