Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Local Testing

Tevm runs a deterministic Ethereum chain in the same process as your test runner. The examples below use the viem-compatible MemoryClient, which is the recommended testing surface in tevm@1.0.0-rc.151.

Setup

npm install tevm@1.0.0-rc.151 viem
npm install --save-dev vitest

No RPC server or separate chain process is required.

Transfer, Mine, and Read a Receipt

Tevm actions can simulate a call without changing canonical state. Set addToMempool: true to create a pending transaction, then mine it explicitly.

import { createMemoryClient, parseEther, PREFUNDED_ACCOUNTS } from 'tevm'
import { expect, test } from 'vitest'
 
test('mines an ETH transfer', async () => {
  const client = createMemoryClient({
    miningConfig: { type: 'manual' },
  })
 
  const alice = PREFUNDED_ACCOUNTS[0].address
  const bob = '0x1111111111111111111111111111111111111111'
 
  const { txHash } = await client.tevmCall({
    from: alice,
    to: bob,
    value: parseEther('1'),
    addToMempool: true,
  })
 
  expect(txHash).toBeDefined()
  if (!txHash) throw new Error('transaction was not added to the txpool')
 
  await client.tevmMine({ blockCount: 1 })
 
  const receipt = await client.getTransactionReceipt({ hash: txHash })
  expect(receipt.status).toBe('success')
  expect(await client.getBalance({ address: bob })).toBe(parseEther('1'))
})

createMemoryClient() is synchronous. Await client.tevmReady() only when a test wants to finish initialization eagerly, especially before reading from a fork.

Test a Contract

This example installs Tevm's bundled SimpleContract bytecode, writes through viem, mines the pending transaction, and reads the resulting state.

import { createMemoryClient, PREFUNDED_ACCOUNTS } from 'tevm'
import { SimpleContract } from 'tevm/contract'
import { expect, test } from 'vitest'
 
test('writes and reads contract state', async () => {
  const client = createMemoryClient({
    miningConfig: { type: 'manual' },
  })
  const contract = SimpleContract.withAddress(
    '0x2222222222222222222222222222222222222222',
  )
 
  await client.setCode({
    address: contract.address,
    bytecode: contract.deployedBytecode,
  })
 
  const hash = await client.writeContract({
    account: PREFUNDED_ACCOUNTS[0],
    address: contract.address,
    abi: contract.abi,
    functionName: 'set',
    args: [42n],
  })
 
  await client.mine({ blocks: 1 })
  await client.waitForTransactionReceipt({ hash })
 
  const value = await client.readContract({
    address: contract.address,
    abi: contract.abi,
    functionName: 'get',
  })
 
  expect(value).toBe(42n)
})

Notice the two mining APIs:

  • client.tevmMine({ blockCount: 1 }) is the Tevm action.
  • client.mine({ blocks: 1 }) is viem's Anvil-compatible test action.

Isolate Tests with Snapshots

Use viem test actions to snapshot and restore the complete local chain.

import { createMemoryClient } from 'tevm'
import { expect, test } from 'vitest'
 
test('restores a snapshot', async () => {
  const client = createMemoryClient()
  const account = '0x3333333333333333333333333333333333333333'
  const snapshotId = await client.snapshot()
 
  await client.setBalance({ address: account, value: 100n })
  expect(await client.getBalance({ address: account })).toBe(100n)
 
  await client.revert({ id: snapshotId })
  expect(await client.getBalance({ address: account })).toBe(0n)
})

Create a fresh client per test when possible. Snapshots are useful when setup is expensive or several assertions need the same baseline.

Control Block Time

The viem test actions control the next block without mutating block objects directly.

import { createMemoryClient } from 'tevm'
import { expect, test } from 'vitest'
 
test('sets the next block timestamp', async () => {
  const client = createMemoryClient()
  const timestamp = 2_000_000_000n
 
  await client.setNextBlockTimestamp({ timestamp })
  await client.mine({ blocks: 1 })
 
  const block = await client.getBlock({ blockTag: 'latest' })
  expect(block.timestamp).toBe(timestamp)
})

For automatic interval mining, configure seconds with miningConfig: { type: 'interval', blockTime: 2 }. Use manual mining in tests that need deterministic transaction boundaries.

Inspect Execution

tevmCall exposes opcode hooks and a Geth-style trace. This is useful for gas assertions and debugger tests.

import { createMemoryClient, PREFUNDED_ACCOUNTS } from 'tevm'
import { expect, test } from 'vitest'
 
test('collects an execution trace', async () => {
  const client = createMemoryClient()
  const contract = '0x4444444444444444444444444444444444444444'
 
  await client.tevmSetAccount({
    address: contract,
    deployedBytecode: '0x6001600055',
  })
 
  const opcodes: string[] = []
  const result = await client.tevmCall({
    from: PREFUNDED_ACCOUNTS[0].address,
    to: contract,
    createTrace: true,
    onStep(step, next) {
      opcodes.push(step.opcode.name)
      next?.()
    },
  })
 
  expect(result.errors).toBeUndefined()
  expect(opcodes).toEqual(['PUSH1', 'PUSH1', 'SSTORE'])
  expect(result.trace?.structLogs).toHaveLength(3)
})

Use the EIP-1193 Surface

The client's request method accepts the EIP-1193 shape: { method, params? }. It does not accept a JSON-RPC envelope with id or jsonrpc.

import { createMemoryClient } from 'tevm'
import { expect, test } from 'vitest'
 
test('serves EIP-1193 requests', async () => {
  const client = createMemoryClient()
 
  const chainId = await client.request({ method: 'eth_chainId' })
  const blockNumber = await client.request({ method: 'eth_blockNumber' })
 
  expect(chainId).toBe('0x384')
  expect(blockNumber).toBe('0x0')
})

Related Topics