CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-openzeppelin--test-helpers

JavaScript testing helpers for Ethereum smart contract development with assertions, event verification, balance tracking, and time manipulation.

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

advanced-features.mddocs/

Advanced Features

Singleton contract deployment, state snapshots, and specialized utilities for complex testing scenarios. Essential for advanced smart contract testing workflows.

Capabilities

ERC1820 Registry Singleton

Deploy or access the ERC1820 Registry singleton contract for interface registration testing.

/**
 * Deploy or get the ERC1820 Registry singleton contract
 * @param {string} funder - Address that will fund the registry deployment (needs 0.08 ETH)
 * @returns {Promise<ContractInstance>} - ERC1820 Registry contract instance
 */
async function singletons.ERC1820Registry(funder);

Usage Examples:

const { singletons, ether, send } = require('@openzeppelin/test-helpers');

contract('ERC777Token', function ([deployer, holder]) {
  beforeEach(async function () {
    // Ensure the ERC1820 Registry is deployed
    this.erc1820 = await singletons.ERC1820Registry(deployer);
    
    // Now deploy ERC777 token that depends on the registry
    this.token = await ERC777.new(
      'MyToken',
      'MTK',
      [deployer], // default operators
      { from: deployer }
    );
  });

  it('should register interface in ERC1820', async function () {
    const interfaceHash = web3.utils.keccak256('ERC777Token');
    
    // Register interface
    await this.erc1820.setInterfaceImplementer(
      this.token.address,
      interfaceHash,
      this.token.address,
      { from: deployer }
    );
    
    // Verify registration
    const implementer = await this.erc1820.getInterfaceImplementer(
      this.token.address,
      interfaceHash
    );
    expect(implementer).to.equal(this.token.address);
  });
});

Blockchain State Snapshots

Create snapshots of blockchain state for efficient test isolation and rollback scenarios.

/**
 * Create a blockchain state snapshot
 * @returns {Promise<SnapshotObject>} - Snapshot object with restore method
 */
async function snapshot();

interface SnapshotObject {
  /**
   * Restore blockchain to the snapshot state
   * @returns {Promise<void>}
   */
  restore(): Promise<void>;
}

Usage Examples:

const { snapshot } = require('@openzeppelin/test-helpers');

contract('StateManagement', function ([owner, user1, user2]) {
  beforeEach(async function () {
    this.contract = await MyContract.new({ from: owner });
    
    // Create snapshot after initial setup
    this.snapshot = await snapshot();
  });

  afterEach(async function () {
    // Restore to initial state after each test
    await this.snapshot.restore();
  });

  it('should modify state in isolation', async function () {
    // Modify contract state
    await this.contract.setValue(100, { from: user1 });
    await this.contract.setOwner(user2, { from: owner });
    
    const value = await this.contract.getValue();
    const newOwner = await this.contract.owner();
    
    expect(value).to.be.bignumber.equal(new BN('100'));
    expect(newOwner).to.equal(user2);
    
    // State will be restored in afterEach
  });

  it('should start with clean state', async function () {
    // This test runs with restored state
    const value = await this.contract.getValue();
    const owner = await this.contract.owner();
    
    expect(value).to.be.bignumber.equal(new BN('0'));
    expect(owner).to.equal(owner);
  });
});

Advanced Snapshot Patterns

const { snapshot, time } = require('@openzeppelin/test-helpers');

contract('TimeBasedContract', function ([owner]) {
  beforeEach(async function () {
    this.contract = await TimeBasedContract.new({ from: owner });
  });

  describe('time-sensitive operations', function () {
    let timeSnapshot;

    beforeEach(async function () {
      // Create snapshot before time manipulation
      timeSnapshot = await snapshot();
    });

    afterEach(async function () {
      // Restore time state
      await timeSnapshot.restore();
    });

    it('should work at different time periods', async function () {
      // Test at current time
      await this.contract.timeBasedFunction();
      
      // Advance time and test again
      await time.increase(time.duration.days(30));
      await this.contract.timeBasedFunction();
      
      // Time and state will be restored after test
    });

    it('should handle multiple time jumps', async function () {
      const results = [];
      
      for (let i = 0; i < 5; i++) {
        await time.increase(time.duration.days(7));
        const result = await this.contract.getWeeklyValue();
        results.push(result);
      }
      
      // Verify progression over time
      expect(results[4]).to.be.bignumber.greaterThan(results[0]);
    });
  });
});

Manual Snapshot Management

const { snapshot } = require('@openzeppelin/test-helpers');

contract('MultiStepProcess', function ([owner, user]) {
  it('should handle complex multi-step scenarios', async function () {
    // Initial setup
    await this.contract.initialize(owner);
    const initialSnapshot = await snapshot();
    
    // Step 1: Configure
    await this.contract.configure(someParameters);
    const configuredSnapshot = await snapshot();
    
    // Test configuration
    const config = await this.contract.getConfiguration();
    expect(config.param1).to.equal(expectedValue);
    
    // Step 2: Process data
    await this.contract.processData(testData);
    
    // Test processing
    const processed = await this.contract.getProcessedData();
    expect(processed.length).to.equal(testData.length);
    
    // Rollback to configured state
    await configuredSnapshot.restore();
    
    // Try alternative processing
    await this.contract.processDataAlternative(alternativeData);
    
    // Test alternative result
    const alternative = await this.contract.getProcessedData();
    expect(alternative.length).to.equal(alternativeData.length);
    
    // Rollback to initial state
    await initialSnapshot.restore();
    
    // Verify complete reset
    const resetConfig = await this.contract.getConfiguration();
    expect(resetConfig.param1).to.equal(defaultValue);
  });
});

Configuration and Environment

Automatic Configuration

The library automatically detects and configures for different environments:

// Truffle environment - uses truffle contracts
// Hardhat environment - uses web3 contracts  
// Standalone - uses web3 contracts

// Manual configuration if needed
const configure = require('@openzeppelin/test-helpers/configure');
configure({
  provider: web3.currentProvider,
  singletons: {
    abstraction: 'web3', // or 'truffle'
    defaultGas: 200000,
    defaultSender: accounts[0],
  }
});

ERC1820 Registry Details

The ERC1820 Registry deployment follows the official EIP-1820 specification:

  • Address: 0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24 (same on all chains)
  • Deployment Cost: 0.08 ETH (transferred to deployment account)
  • Deployment Method: Pre-signed transaction broadcast
// Registry ABI methods available
const registry = await singletons.ERC1820Registry(funder);

// Set interface implementer
await registry.setInterfaceImplementer(addr, interfaceHash, implementer);

// Get interface implementer  
const implementer = await registry.getInterfaceImplementer(addr, interfaceHash);

// Get manager for address
const manager = await registry.getManager(addr);

// Set manager for address
await registry.setManager(addr, newManager);

Performance Considerations

Snapshot Efficiency

Snapshots are more efficient than resetting state manually:

// Efficient - uses blockchain snapshot
beforeEach(async function () {
  await this.snapshot.restore();
});

// Inefficient - redeploys contracts
beforeEach(async function () {
  this.contract = await MyContract.new();
  await this.contract.initialize();
  await this.contract.setupInitialState();
});

Registry Deployment

The ERC1820 Registry is deployed once per test session and reused:

// First call deploys registry (expensive)
const registry1 = await singletons.ERC1820Registry(funder);

// Subsequent calls return existing registry (fast)
const registry2 = await singletons.ERC1820Registry(funder);

// registry1.address === registry2.address

Error Handling

const { singletons, snapshot } = require('@openzeppelin/test-helpers');

// Handle insufficient funds for registry deployment
try {
  const registry = await singletons.ERC1820Registry(poorAccount);
} catch (error) {
  console.error('Cannot deploy registry:', error.message);
  // Fund the account or use a different funder
}

// Handle snapshot restore failures
try {
  await this.snapshot.restore();
} catch (error) {
  console.error('Snapshot restore failed:', error.message);
  // Create new snapshot or handle failure
}

Integration Examples

Testing ERC777 with Registry

const { singletons, expectEvent, ether } = require('@openzeppelin/test-helpers');

contract('ERC777Integration', function ([deployer, holder, operator]) {
  beforeEach(async function () {
    // Deploy registry first
    this.erc1820 = await singletons.ERC1820Registry(deployer);
    
    // Deploy ERC777 token
    this.token = await ERC777.new(
      'Test Token',
      'TEST',
      [operator],
      { from: deployer }
    );
    
    // Create snapshot for test isolation
    this.snapshot = await snapshot();
  });

  afterEach(async function () {
    await this.snapshot.restore();
  });

  it('should register and use token recipient', async function () {
    // Deploy token recipient contract
    this.recipient = await TokenRecipient.new({ from: holder });
    
    // Register recipient interface
    const recipientHash = web3.utils.keccak256('ERC777TokensRecipient');
    await this.erc1820.setInterfaceImplementer(
      holder,
      recipientHash,
      this.recipient.address,
      { from: holder }
    );
    
    // Send tokens - should trigger recipient
    const receipt = await this.token.send(
      holder,
      ether('1'),
      '0x',
      { from: deployer }
    );
    
    // Verify recipient was called
    expectEvent.inTransaction(
      receipt.tx,
      this.recipient,
      'TokensReceived'
    );
  });
});

Install with Tessl CLI

npx tessl i tessl/npm-openzeppelin--test-helpers

docs

advanced-features.md

balance-tracking.md

constants-utilities.md

event-testing.md

index.md

revert-testing.md

time-manipulation.md

tile.json