back to posts
#08 Part 2 2025-08-28 22 min

BigInt Testing Hell: Mocking Blockchain Numbers in Jest

How JavaScript's BigInt type turned our test suites into debugging nightmares

Part 2 of the Journey: Discovering the Patterns - Learning Through Pain Previous: Error Handling in MCP | Next: The Cross-Chain Tool Naming Registry

BigInt Testing Hell: Mocking Blockchain Numbers in Jest

How JavaScript’s BigInt type turned our test suites into debugging nightmares

Historical Context (November 2025): Encountered August 2025 while building testing infrastructure for blockchain servers. BigInt testing patterns for MCP weren’t documented—we solved this through experimentation.

Date: August 28, 2025 Author: Myron Koch & Claude Code Category: Testing War Stories

The Innocent Beginning

// Simple test, right?
describe('getBalance', () => {
  it('should return wallet balance', async () => {
    const mockClient = {
      getBalance: jest.fn().mockResolvedValue(1000000000000000000)
    };

    const result = await handleGetBalance({ address: '0x123' }, mockClient);
    expect(result.content[0].text).toContain('1000000000000000000');
  });
});

Test result:

FAIL  tests/core.test.ts
  ● getBalance › should return wallet balance

    TypeError: Cannot mix BigInt and other types, use explicit conversions

Welcome to BigInt hell.

The Problem

Blockchain numbers are HUGE:

JavaScript’s Number type: Max safe integer = 9,007,199,254,740,991

One ETH in wei: 1,000,000,000,000,000,000

JavaScript Number literally cannot hold blockchain values.

Enter BigInt

JavaScript’s BigInt to the rescue! But with a catch:

// BigInt rules that will ruin your day:
const a = BigInt(10);
const b = 20;

a + b;        // 💥 TypeError: Cannot mix BigInt and other types
a + BigInt(b); // ✅ 30n

JSON.stringify({ value: a }); // 💥 TypeError: Do not know how to serialize a BigInt

console.log(a); // ✅ Works
`Value: ${a}`;  // ✅ Works (coercion)
a.toString();   // ✅ Works

The Jest Mocking Nightmare

Attempt 1: Mock with Numbers

mockClient.getBalance.mockResolvedValue(1000000000000000000);
// 💥 Runtime: Cannot convert 1000000000000000000 to BigInt

Attempt 2: Mock with BigInt

mockClient.getBalance.mockResolvedValue(BigInt('1000000000000000000'));
// 💥 Jest: Cannot serialize BigInt in snapshot

Attempt 3: Mock with String

mockClient.getBalance.mockResolvedValue('1000000000000000000');
// 💥 Application: Expected BigInt, got string

Attempt 4: The “n” Notation

mockClient.getBalance.mockResolvedValue(1000000000000000000n);
// 💥 Jest: Unexpected token 'n'

The Type System Chaos

TypeScript made it worse:

interface Balance {
  value: bigint;  // TypeScript type
}

const mockBalance: Balance = {
  value: BigInt(1000)  // Runtime value
};

// In tests:
expect(result.value).toBe(BigInt(1000));
// 💥 Received: serializes to the same string

expect(result.value).toBe(1000n);
// 💥 Parsing error: Unexpected token

expect(result.value.toString()).toBe('1000');
// ✅ Finally works, but loses type safety

The JSON.stringify Apocalypse

Our MCP servers return JSON responses:

return {
  content: [{
    type: 'text',
    text: JSON.stringify({ balance: BigInt(1000) })
    // 💥 TypeError: Do not know how to serialize a BigInt
  }]
};

The “fix”:

// The BigInt JSON hack everyone uses
JSON.stringify(obj, (_, v) => typeof v === 'bigint' ? v.toString() : v);

But this breaks EVERYWHERE:

// Have to add replacer to every JSON.stringify
JSON.stringify(data, bigIntReplacer);
JSON.stringify(response, bigIntReplacer);
JSON.stringify(config, bigIntReplacer);
// Miss one? 💥 Runtime explosion

The Testing Solutions That Actually Work

Solution 1: The BigInt Test Helper

// tests/helpers/bigint.ts
export function mockBigInt(value: string | number): any {
  const bigIntValue = BigInt(value);

  // Return object that behaves like BigInt in tests
  return {
    toString: () => bigIntValue.toString(),
    valueOf: () => bigIntValue,
    [Symbol.toStringTag]: 'BigInt',
    // Add math operations as needed
    add: (other: any) => mockBigInt((bigIntValue + BigInt(other)).toString()),
    sub: (other: any) => mockBigInt((bigIntValue - BigInt(other)).toString()),
    mul: (other: any) => mockBigInt((bigIntValue * BigInt(other)).toString()),
  };
}

// Usage in tests:
mockClient.getBalance.mockResolvedValue(mockBigInt('1000000000000000000'));

Solution 2: The String Protocol

// Everywhere in the app: Convert BigInt to string immediately
async function getBalance(address: string): Promise<string> {
  const balance = await web3.eth.getBalance(address);  // Returns BigInt
  return balance.toString();  // Always stringify immediately
}

// In tests: Just use strings
mockClient.getBalance.mockResolvedValue('1000000000000000000');

Solution 3: The Wrapper Pattern

// Create a Balance class that handles serialization
class Balance {
  private value: bigint;

  constructor(value: string | bigint) {
    this.value = BigInt(value);
  }

  toString(): string {
    return this.value.toString();
  }

  toJSON(): string {
    return this.toString();
  }

  add(other: Balance): Balance {
    return new Balance(this.value + other.value);
  }
}

// Tests become manageable
const mockBalance = new Balance('1000000000000000000');
mockClient.getBalance.mockResolvedValue(mockBalance);

The Jest Configuration Battle

Getting Jest to handle BigInt:

// jest.config.cjs
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  globals: {
    'ts-jest': {
      tsconfig: {
        target: 'ES2020',  // Supports BigInt
      },
    },
  },
  // Custom serializer for BigInt
  snapshotSerializers: ['./tests/bigint-serializer.js'],
};

// tests/bigint-serializer.js
module.exports = {
  serialize(val) {
    return val.toString() + 'n';
  },
  test(val) {
    return typeof val === 'bigint';
  },
};

Real-World Test Failures We Hit

The Comparison Trap

// Test
expect(result.balance).toBeGreaterThan(0);
// 💥 Cannot compare BigInt with number

// Fix
expect(result.balance).toBeGreaterThan(BigInt(0));

The Math Operation Disaster

// Test calculating fees
const fee = mockBalance * 0.01;
// 💥 Cannot mix BigInt and other types

// Fix
const fee = (mockBalance * BigInt(1)) / BigInt(100);

The Array Sum Nightmare

// Summing balances
const total = balances.reduce((sum, bal) => sum + bal, 0);
// 💥 Cannot mix BigInt and number

// Fix
const total = balances.reduce((sum, bal) => sum + bal, BigInt(0));

The Patterns We Developed

1. The BigInt Factory

// utils/bigint.ts
export function toBigInt(value: string | number | bigint): bigint {
  if (typeof value === 'bigint') return value;
  if (typeof value === 'string') return BigInt(value);
  if (Number.isSafeInteger(value)) return BigInt(value);
  throw new Error(`Cannot convert ${value} to BigInt safely`);
}

// Always use the factory
const balance = toBigInt(mockValue);

2. The Safe Math Module

// utils/safeMath.ts
export const SafeMath = {
  add: (a: string, b: string): string => {
    return (BigInt(a) + BigInt(b)).toString();
  },
  sub: (a: string, b: string): string => {
    return (BigInt(a) - BigInt(b)).toString();
  },
  mul: (a: string, b: string): string => {
    return (BigInt(a) * BigInt(b)).toString();
  },
  div: (a: string, b: string): string => {
    return (BigInt(a) / BigInt(b)).toString();
  },
};

3. The Test Data Builder

// tests/builders/blockchain.ts
export class BlockchainDataBuilder {
  static balance(value: string = '1000000000000000000'): any {
    return {
      raw: BigInt(value),
      toString: () => value,
      toJSON: () => value,
      formatted: () => `${BigInt(value) / BigInt(1e18)} ETH`,  // BigInt-safe division
    };
  }

  static transaction(overrides = {}): any {
    return {
      hash: '0x123...',
      value: this.balance(),
      gasPrice: BigInt('20000000000'),
      gasLimit: BigInt('21000'),
      ...overrides,
    };
  }
}

The Current State of Our Tests

After months of battle:

// A working test that handles BigInt properly
describe('Ethereum Balance Operations', () => {
  let mockClient: any;

  beforeEach(() => {
    mockClient = {
      getBalance: jest.fn(),
      // Use string protocol for consistency
      formatBalance: jest.fn((wei: string) => {
        const eth = Number(wei) / 1e18;
        return `${eth.toFixed(4)} ETH`;
      }),
    };
  });

  it('should handle large balances correctly', async () => {
    const hugeBalance = '999999999999999999999999'; // Whale alert!

    mockClient.getBalance.mockResolvedValue(hugeBalance);

    const result = await handleGetBalance(
      { address: '0xwhale' },
      mockClient
    );

    const parsed = JSON.parse(result.content[0].text);
    expect(parsed.balance).toBe(hugeBalance);
    expect(parsed.formatted).toContain('ETH');

    // Validate it's actually a huge number
    expect(BigInt(parsed.balance)).toBeGreaterThan(
      BigInt('999999999999999999999998')
    );
  });
});

The Checklist for BigInt Testing

When testing blockchain code:

The Philosophical Question

Should blockchain values be BigInt at all?

The String Camp says: “Just use strings everywhere. No type confusion.”

The BigInt Camp says: “We need math operations. BigInt is correct.”

The Reality: We use BigInt internally, convert to strings at boundaries.

The Future

When will this get better?

Until then, we live in BigInt testing hell.

References


This is part of our ongoing series documenting architectural patterns and insights from building the Blockchain MCP Server Ecosystem. Some battles you don’t win, you just survive.


Prerequisites

Next Steps

Deep Dives