I found openzeppelin-test-environment solving my problem. It allows ephemeral Ethereum blockchain setup, contract deployment, etc. relatively easy.
Below is my original Truffle + TypeScript test translated to OpenZeppelin + Jest + power-assert.
Jest test
import assert = require('assert');
import { accounts, contract } from '@openzeppelin/test-environment';
import {
BN, // Big Number support
constants, // Common constants, like the zero address and largest integers
} from '@openzeppelin/test-helpers';
// https://etherscan.io/address/0xaf30d2a7e90d7dc361c8c4585e9bb7d2f6f15bc7#readContract
const TOKEN_1ST_TOTAL_SUPPLY = new BN('93468683899196345527500000');
// Ethereum accounts used in these tests
const [
deployer, // Deploys the smart contract
owner, // Owns the initial supply
user2 // Random dude who wants play with tokens
] = accounts;
// Loads a compiled contract using OpenZeppelin test-environment
const Dawn = contract.fromArtifact('Dawn');
beforeEach(() => {
// No setup
});
afterEach(() => {
// No setup
});
test('The supply should match original token', async () => {
const token = await Dawn.new(owner, { from: deployer });
const supply = await token.totalSupply();
// Big number does not have power-assert support yet - https://github.com/power-assert-js/power-assert/issues/124
assert(supply.toString() == TOKEN_1ST_TOTAL_SUPPLY.toString());
});
test("Token should allow transfer", async () => {
const token = await Dawn.new(owner, { from: deployer });
const amount = new BN("1") * new BN("1e18"); // Transfer 1 whole token
await token.transfer(user2, amount, { from: owner });
const balanceAfter = await token.balanceOf(user2);
assert(balanceAfter.toString() == amount.toString());
});
test("Token tranfers are disabled after pause", async () => {
const token = await Dawn.new(owner, { from: deployer });
const amount = new BN("1") * new BN("1e18"); // Transfer 1 whole token
// Pause
await token.pause({ from: owner });
assert(await token.paused());
// Transfer tokens fails after the pause
assert.rejects(async () => {
await token.transfer(user2, amount, { from: owner });
});
});
test("Token tranfers can be paused by the owner only", async () => {
const token = await Dawn.new(owner, { from: deployer });
const amount = new BN("1") * new BN("1e18"); // Transfer 1 whole token
// Transfer tokens fails after the pause
assert.rejects(async () => {
await token.pause({ from: user2 });
});
});
test("Token cannot be send to 0x0 null address by accident", async () => {
const token = await Dawn.new(owner, { from: deployer });
const amount = new BN("1") * new BN("1e18"); // Transfer 1 whole token
assert.rejects(async () => {
await token.transfer(constants.ZERO_ADDRESS, amount, { from: owner });
});
});
Original Truffle test (Mocha + power-assert)
const Dawn = artifacts.require("Dawn");
var assert = require('assert'); // Power assert https://github.com/power-assert-js/espower-typescript
// https://etherscan.io/address/0xaf30d2a7e90d7dc361c8c4585e9bb7d2f6f15bc7#readContract
const TOKEN_1ST_TOTAL_SUPPLY = web3.utils.toBN('93468683899196345527500000');
contract("Dawn", ([deployer, user1, user2]) => {
const tokenOwner = user1;
it("should have total supply of 1ST token after deploy", async () => {
const dawn = await Dawn.new(tokenOwner, { from: deployer });
const supply = await dawn.totalSupply();
assert(supply.toString() == TOKEN_1ST_TOTAL_SUPPLY.toString());
});
it("should allow transfer", async () => {
const dawn = await Dawn.new(tokenOwner, { from: deployer });
const amount = web3.utils.toWei("1", "ether"); // 1 full token
// Transfer tokens
await dawn.transfer(user2, amount, { from: tokenOwner });
const balanceAfter = await dawn.balanceOf(user2);
assert(balanceAfter.toString() == amount.toString());
});
it("should not allow transfers after pause", async () => {
const dawn = await Dawn.new(tokenOwner, { from: deployer });
const amount = web3.utils.toWei("1", "ether"); // 1 full token
// Pause
await dawn.pause({ from: tokenOwner });
assert(await dawn.paused());
// Transfer tokens fails
assert.rejects(async () => {
await dawn.transfer(user2, amount, { from: user1 });
});
});
it("should not allow pause by a random person", async () => {
const dawn = await Dawn.new(tokenOwner, { from: deployer });
// Transfer tokens fails
assert.rejects(async () => {
await dawn.pause({ from: user2 });
});
});
});
My package.json
{
"name": "dawntoken",
"version": "1.0.0",
"description": "",
"main": "truffle.js",
"directories": {
"test": "test"
},
"scripts": {
"generate": "truffle compile && typechain --target truffle './build/**/*.json'",
"test": "jest",
"tsc": "tsc --noEmit"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@openzeppelin/test-environment": "^0.1.3",
"@openzeppelin/test-helpers": "^0.5.4",
"@types/jest": "^25.1.3",
"@types/power-assert": "^1.5.3",
"babel-jest": "^25.1.0",
"babel-preset-power-assert": "^3.0.0",
"espower-typescript": "^9.0.2",
"jest": "^25.1.0",
"power-assert": "^1.6.1",
"ts-jest": "^25.2.1",
"ts-node": "^8.6.2",
"typescript": "^3.8.3",
"babel-core": "^6.26.3"
},
"dependencies": {
"@truffle/hdwallet-provider": "^1.0.32",
"bignumber.js": "^9.0.0",
"dotenv": "^8.2.0",
"ganache-core": "^2.10.2",
"openzeppelin-solidity": "^2.5.0",
"solc": "^0.6.3",
"truffle": "^5.1.16",
"web3": "^1.2.6"
},
"jest": {
"verbose": true,
"preset": "ts-jest",
"testMatch": ["**/tests/*.ts"],
"testEnvironment": "node",
"globals": {
"ts-jest": {
"babelConfig": {
"presets": [
"power-assert"
]
}
}
}
}
}
For further information how to set up Jest + TypeScript + power-assert look here.