Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ponder test #1410

Open
typedarray opened this issue Jan 6, 2025 · 2 comments
Open

feat: ponder test #1410

typedarray opened this issue Jan 6, 2025 · 2 comments

Comments

@typedarray
Copy link
Collaborator

Problem / use case

It can be useful to write unit tests for indexing functions to confirm their behavior over a specific sequence of events.

It's not always convenient (or possible?) to validate your indexing logic only using ponder dev and ponder start. For example: you have a specific sequence of events that appears to cause an indexing error. You can't run only the snippet of history in question, because it relies on older events. Your app is large, so it takes minutes or hours to reproduce the error. This is a bad feedback loop for development.

To solve for this, we could introduce a pattern to unit test indexing functions. This way, you could manually construct the sequence of events to rapidly reproduce and fix the error.

Proposed solution

The general flow of a test could be:

  1. Write or generate one (or more) mock event objects
  2. Pass the events to an indexing function, or run them through "the entire app"
  3. Assert things about the database state

This could go in two directions - a native, opinionated ponder test command, or some way to run indexing functions "headlessly" like Hono's testing helper. I personally lean towards a native command.

IMO, the most obvious choice would be a very thin wrapper over Vitest - we already use Vite internally to execute user code, and we use Vitest for the framework test suite so we're familiar with it.

A basic test could look something like this:

import { expect, test } from "vitest"

test("PairCreated inserts Pool row", (context) => {
  const event = createEvent(...) // TODO: what does this API look like?
  event.block.timestamp = 123

  await context.executeFunction("UniswapFactory:PairCreated", event)

  const row =  await context.db.pool.findFirst({ ... })
  expect(row.timestamp).toBe(123)
})

The hardest part will be designing an intuitive API for building or generating the mock event data.

@dsldsl
Copy link

dsldsl commented Jan 6, 2025

Thanks @typedarray! I'll share some thoughts, though I'm newish to this domain so pls contextualize accordingly.

I think my ideal scenario would look like this in the end for me as a dev:

// Hypothetical DEX "Myswap" testing.

// Define test scenarios using ABIs
const MyswapTestScenarios = {
  poolCreated: (tokenA: Address, tokenB: Address, poolAddress: Address) => LogBuilder.buildLogFromAbi({
    contractAddress: ADDRESSES.MYSWAP_FACTORY,
    abi: factoryAbi,
    eventName: 'PoolCreated',
    args: [tokenA, tokenB, poolAddress]
  }),

  swap: (params: {
    poolAddress: Address,
    sender: Address,
    recipient: Address,
    amountIn: bigint,
    amountOut: bigint,
    tokenAIn: boolean
  }) => LogBuilder.buildLogFromAbi({
    contractAddress: params.poolAddress,
    abi: poolAbi,
    eventName: 'PoolSwap',
    args: [
      params.sender,
      params.recipient,
      params.amountIn,
      params.amountOut,
      params.tokenAIn
    ]
  })
};

// Tests remain the same
describe('MySwap Handler', () => {
  it('handles pool creation correctly', async () => {
    const log = MyswapTestScenarios.poolCreated(
      ADDRESSES.USDC,
      ADDRESSES.WETH,
      '0xnewpool...'
    );
    
    // Assert DB state
  });

  it('handles swap correctly', async () => {
    const log = MyswapTestScenarios.swap({
      poolAddress: '0xpool...',
      sender: '0xuser...',
      recipient: '0xuser...',
      amountIn: BigInt(1000000),
      amountOut: BigInt(950000),
      tokenAIn: true
    });
    
    // Assert DB state
  });
});

So that makes the testing flow pretty clean and straightforward.

LogBuilder I think would be a provided Ponder utility (or maybe something exists already) to create the properly encoded topics array and data field. The buildLog function converts human-readable inputs and handles all the low-level details of encoding them into the format needed for synthetic log testing.

And then I'm sure there's a smart way to integrate the above MyswapTestScenarios with the existing Ponder config to avoid duplication, but directionally the above is what I'd think.

@tk-o
Copy link
Contributor

tk-o commented Jan 9, 2025

I was testing ponder indexing on the public API level.

  1. I'd set up a docker compose file where I'd run a Foundry chain and the Ponder indexer app.
  2. The testing suit would call the RPC over local network and a smart contract emit an given event. This way, and the indexer would have it indexed, and the testing suite would wait for the ponder gql api to return the expected result across selected aggregates (defined in ponder.schema.ts, populated in the indexing functions).

There was not need to mock anything, but I appreciate this setup would not work well if I had to index some third-party contracts that I could not deploy to the local network.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants