Skip to content

Local Testing

These docs have not been checked for correctness yet. Use with caution

This guide demonstrates how to use Tevm Node for local testing of smart contracts and transactions. For more background on testing Ethereum applications, see the Smart Contract Testing Guide.

Basic Test Setup

import { createTevmNode } from 'tevm'
import { createImpersonatedTx } from 'tevm/tx'
import { expect, test } from 'vitest' // or jest, mocha, etc.
 
test('Basic ETH transfer', async () => {
  // Create a new node instance
  const node = createTevmNode({
    miningConfig: { type: 'auto' }, // Mine blocks automatically
  })
  await node.ready()
 
  const vm = await node.getVm()
 
  // Create and run a transaction
  const tx = createImpersonatedTx({
    from: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
    to: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',
    value: 1000000000000000000n, // 1 ETH
  })
 
  const result = await vm.runTx({ tx })
 
  // Assert transaction succeeded
  expect(result.execResult.exceptionError).toBeUndefined()
 
  // Check recipient balance
  const account = await vm.stateManager.getAccount(tx.to)
  expect(account.balance).toBe(1000000000000000000n)
})

For more information on transaction execution, see the EVM Execution Model.

Contract Testing

1. Deployment & Interaction

For background on contract deployment and interaction, see the Contract Deployment Guide.

import { Contract } from 'tevm/contract'
import { bytecode, abi } from './MyContract.json'
 
test('Deploy and interact with contract', async () => {
  const node = createTevmNode()
  await node.ready()
  const vm = await node.getVm()
 
  // Deploy contract
  const deployTx = createImpersonatedTx({
    data: bytecode,
  })
 
  const result = await vm.runTx({ tx: deployTx })
  expect(result.execResult.exceptionError).toBeUndefined()
 
  const contractAddress = result.createdAddress
  expect(contractAddress).toBeDefined()
 
  // Create contract instance
  const contract = new Contract(contractAddress, abi)
 
  // Call contract method
  const callResult = await contract.read.getValue()
  expect(callResult).toBe(expectedValue)
 
  // Send transaction to contract
  const tx = await contract.write.setValue([newValue])
  const txResult = await vm.runTx({ tx })
  expect(txResult.execResult.exceptionError).toBeUndefined()
 
  // Verify state change
  const updatedValue = await contract.read.getValue()
  expect(updatedValue).toBe(newValue)
})

2. Event Testing

For more information on events and logs, see the Events and Logs Guide.

test('Contract events', async () => {
  const node = createTevmNode()
  await node.ready()
 
  // Deploy contract
  const contract = await deployContract(node)
 
  // Create event filter
  node.setFilter({
    id: '0x1',
    address: contract.address,
    topics: [
      contract.interface.getEventTopic('ValueChanged'),
    ],
  })
 
  // Trigger event
  const tx = await contract.write.setValue([123])
  await vm.runTx({ tx })
 
  // Get event logs
  const receipts = await node.getReceiptsManager()
  const logs = await receipts.getLogs({
    fromBlock: 0n,
    toBlock: 'latest',
    address: contract.address,
  })
 
  expect(logs.length).toBe(1)
  expect(logs[0].topics[0]).toBe(contract.interface.getEventTopic('ValueChanged'))
})

Complex Testing Scenarios

1. State Management

For more information on state management, see the Ethereum State Guide.

test('Complex state changes', async () => {
  const node = createTevmNode()
  await node.ready()
  const vm = await node.getVm()
 
  // Create checkpoint
  await vm.stateManager.checkpoint()
 
  try {
    // Perform multiple state changes
    await performStateChanges(vm)
 
    // Verify intermediate state
    const intermediateState = await getState(vm)
    expect(intermediateState).toMatchSnapshot()
 
    // More changes
    await performMoreChanges(vm)
 
    // Commit changes
    await vm.stateManager.commit()
  } catch (error) {
    // Revert on failure
    await vm.stateManager.revert()
    throw error
  }
})

2. Fork Testing

For more information on network forking, see the Forking Guide.

test('Mainnet fork testing', async () => {
  const node = createTevmNode({
    fork: {
      transport: http('https://mainnet.infura.io/v3/YOUR-KEY'),
      blockTag: 17_000_000n,
    },
  })
  await node.ready()
 
  // Impersonate a whale account
  node.setImpersonatedAccount('0x28C6c06298d514Db089934071355E5743bf21d60')
 
  // Test DeFi interactions
  const uniswap = new Contract(UNISWAP_ADDRESS, UNISWAP_ABI)
  const tx = await uniswap.write.swapExactTokensForTokens([/* ... */])
 
  const result = await vm.runTx({ tx })
  expect(result.execResult.exceptionError).toBeUndefined()
})

3. Time-based Testing

For more information on block timestamps and time-based operations, see the Block Time Guide.

test('Time-dependent behavior', async () => {
  const node = createTevmNode({
    miningConfig: { type: 'interval', interval: 1000 },
  })
  await node.ready()
  const vm = await node.getVm()
 
  // Deploy time-locked contract
  const contract = await deployTimeLock(vm)
 
  // Try to withdraw (should fail)
  let tx = await contract.write.withdraw()
  let result = await vm.runTx({ tx })
  expect(result.execResult.exceptionError).toBeDefined()
 
  // Advance time by mining blocks
  for (let i = 0; i < 100; i++) {
    await vm.blockchain.putBlock(createBlock({ timestamp: Date.now() + i * 1000 }))
  }
 
  // Try withdraw again (should succeed)
  tx = await contract.write.withdraw()
  result = await vm.runTx({ tx })
  expect(result.execResult.exceptionError).toBeUndefined()
})

Testing Utilities

1. Account Management

For more information on Ethereum accounts, see the Accounts Guide.

// Helper to setup test accounts
async function setupAccounts(vm) {
  const accounts = [
    '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
    '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',
  ]
 
  for (const address of accounts) {
    await vm.stateManager.putAccount(address, {
      nonce: 0n,
      balance: 10000000000000000000n, // 10 ETH
    })
  }
 
  return accounts
}

2. Transaction Helpers

For more information on transaction types and formats, see the Transaction Types Guide.

// Helper to send ETH
async function sendEth(vm, from, to, value) {
  const tx = createImpersonatedTx({
    from,
    to,
    value,
  })
 
  return vm.runTx({ tx })
}
 
// Helper to deploy contract
async function deployContract(vm, bytecode, args = []) {
  const tx = createImpersonatedTx({
    data: bytecode + encodeConstructor(args),
  })
 
  const result = await vm.runTx({ tx })
  return result.createdAddress
}

Related Topics

Replaying Contracts with Shadow Events

import { createTevmNode } from 'tevm'
import { http } from 'viem'
 
// Create a node that forks from mainnet
const node = createTevmNode({
  fork: {
    transport: http('https://mainnet.infura.io/v3/YOUR-KEY'),
  },
})
 
// Get the transaction receipt to find its index
const receipt = await node.request({
  method: 'eth_getTransactionReceipt',
  params: ['0x...'] // Original tx hash
})
 
// Get the block and its transactions
const block = await node.request({
  method: 'eth_getBlockByNumber',
  params: [(receipt.blockNumber - 1n).toString(16), true]
})
 
// Replay all transactions before our target tx
for (let i = 0; i < receipt.transactionIndex; i++) {
  const tx = block.transactions[i]
  await node.getVm().runTx({ tx })
}
 
// Deploy modified contract with new event
const modifiedBytecode = '0x...' // Contract bytecode with new event
await node.setAccount({
  address: receipt.contractAddress,
  deployedBytecode: modifiedBytecode
})
 
// Now run the target transaction
const result = await node.getVm().runTx({
  tx: block.transactions[receipt.transactionIndex]
})
 
// The result will include the new shadow event
console.log(result.execResult.logs)

Estimating Gas for Token Approval

import { createTevmNode } from 'tevm/node'
import { encodeFunctionData } from 'viem'
 
const node = createTevmNode()
const vm = await node.getVm()
 
// First approve the token
const approveTx = {
  to: tokenAddress,
  data: encodeFunctionData({
    abi: erc20ABI,
    functionName: 'approve',
    args: [spenderAddress, amount]
  })
}
 
// Estimate gas by running the tx
const result = await vm.runTx({ tx: approveTx })
console.log('Gas used:', result.execResult.executionGasUsed)
 
// Now we can estimate transferFrom
const transferFromTx = {
  to: tokenAddress,
  data: encodeFunctionData({
    abi: erc20ABI,
    functionName: 'transferFrom',
    args: [ownerAddress, recipientAddress, amount]
  })
}
 
const transferResult = await vm.runTx({ tx: transferFromTx })
console.log('TransferFrom gas:', transferResult.execResult.executionGasUsed)