Skip to content

Testing Gateway Connectors

Overview

Comprehensive testing is crucial for Gateway connectors to ensure reliability, security, and compatibility. This guide covers testing strategies, tools, and best practices for Gateway connector development.

Testing Requirements

All Gateway connectors must meet these minimum requirements:

  • Code Coverage: ≥75% overall coverage
  • Unit Tests: All public methods tested
  • Integration Tests: All API endpoints tested
  • Error Cases: All failure scenarios covered
  • Performance: Response times under load tested

Test Structure

Directory Organization

test/
├── connectors/
│   └── mydex/
│       ├── mydex.test.ts           # Unit tests
│       ├── mydex.integration.test.ts # Integration tests
│       └── fixtures/                # Test data
│           ├── tokens.json
│           ├── pools.json
│           └── transactions.json
├── mocks/
│   └── mydex/
│       ├── sdk.mock.ts            # SDK mocks
│       └── responses.mock.ts      # API response mocks
└── utils/
    ├── test-helpers.ts            # Shared utilities
    └── test-constants.ts          # Common test data

Unit Testing

Basic Test Structure

import { MyDex } from '../../../src/connectors/mydex/mydex';
import { MockSDK } from '../../mocks/mydex/sdk.mock';
import { fixtures } from './fixtures';

describe('MyDex Connector', () => {
  let connector: MyDex;
  let mockSDK: MockSDK;

  beforeEach(() => {
    mockSDK = new MockSDK();
    connector = new MyDex('ethereum', 'mainnet', mockSDK);
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  describe('initialization', () => {
    it('should initialize with correct chain and network', () => {
      expect(connector.chain).toBe('ethereum');
      expect(connector.network).toBe('mainnet');
    });

    it('should load configuration correctly', () => {
      expect(connector.config).toBeDefined();
      expect(connector.config.allowedSlippage).toBe(1.0);
    });
  });
});

Testing Swap Operations

describe('swap operations', () => {
  describe('quote', () => {
    it('should return valid quote for token swap', async () => {
      const quote = await connector.quote({
        base: fixtures.tokens.USDC,
        quote: fixtures.tokens.WETH,
        amount: '1000000', // 1 USDC
        side: 'SELL'
      });

      expect(quote).toMatchObject({
        expectedOut: expect.any(String),
        price: expect.any(String),
        priceImpact: expect.any(Number),
        route: expect.any(Array)
      });

      expect(Number(quote.expectedOut)).toBeGreaterThan(0);
      expect(quote.priceImpact).toBeLessThan(0.1);
    });

    it('should handle buy side quotes', async () => {
      const quote = await connector.quote({
        base: fixtures.tokens.USDC,
        quote: fixtures.tokens.WETH,
        amount: '1000000000000000', // 0.001 WETH
        side: 'BUY'
      });

      expect(quote.expectedOut).toBeDefined();
    });

    it('should throw on insufficient liquidity', async () => {
      mockSDK.setLiquidity(0);

      await expect(connector.quote({
        base: fixtures.tokens.USDC,
        quote: fixtures.tokens.WETH,
        amount: '999999999999999999',
        side: 'SELL'
      })).rejects.toThrow('Insufficient liquidity');
    });
  });

  describe('trade', () => {
    it('should execute swap successfully', async () => {
      const tx = await connector.trade({
        wallet: fixtures.wallet,
        base: fixtures.tokens.USDC,
        quote: fixtures.tokens.WETH,
        amount: '1000000',
        side: 'SELL',
        slippage: 0.01
      });

      expect(tx).toMatchObject({
        hash: expect.stringMatching(/^0x/),
        gasUsed: expect.any(String),
        status: 'success'
      });
    });

    it('should respect slippage settings', async () => {
      const spy = jest.spyOn(mockSDK, 'swap');

      await connector.trade({
        wallet: fixtures.wallet,
        base: fixtures.tokens.USDC,
        quote: fixtures.tokens.WETH,
        amount: '1000000',
        side: 'SELL',
        slippage: 0.005
      });

      expect(spy).toHaveBeenCalledWith(
        expect.objectContaining({
          minAmountOut: expect.any(String)
        })
      );
    });
  });
});

Testing Liquidity Operations

describe('liquidity operations', () => {
  describe('addLiquidity', () => {
    it('should add liquidity to pool', async () => {
      const tx = await connector.addLiquidity({
        wallet: fixtures.wallet,
        pool: fixtures.pools.USDC_WETH,
        baseAmount: '1000000',
        quoteAmount: '1000000000000000',
        slippage: 0.01
      });

      expect(tx.hash).toBeDefined();
      expect(tx.lpTokensReceived).toBeGreaterThan(0);
    });

    it('should calculate correct token ratios', async () => {
      const result = await connector.quoteLiquidity({
        pool: fixtures.pools.USDC_WETH,
        baseAmount: '1000000'
      });

      expect(result.quoteAmount).toBeDefined();
      expect(result.shareOfPool).toBeGreaterThan(0);
      expect(result.shareOfPool).toBeLessThan(1);
    });
  });

  describe('removeLiquidity', () => {
    it('should remove liquidity from pool', async () => {
      const tx = await connector.removeLiquidity({
        wallet: fixtures.wallet,
        pool: fixtures.pools.USDC_WETH,
        liquidity: '1000000000000000000',
        slippage: 0.01
      });

      expect(tx.baseAmountReceived).toBeGreaterThan(0);
      expect(tx.quoteAmountReceived).toBeGreaterThan(0);
    });
  });
});

Integration Testing

API Endpoint Testing

import request from 'supertest';
import { app } from '../../../src/app';

describe('MyDex API Endpoints', () => {
  describe('POST /connectors/mydex/router/quote-swap', () => {
    it('should return swap quote', async () => {
      const response = await request(app)
        .post('/connectors/mydex/router/quote-swap')
        .send({
          chain: 'ethereum',
          network: 'mainnet',
          connector: 'mydex',
          base: 'USDC',
          quote: 'WETH',
          amount: '1000000',
          side: 'SELL'
        });

      expect(response.status).toBe(200);
      expect(response.body).toMatchObject({
        network: 'mainnet',
        base: 'USDC',
        quote: 'WETH',
        expectedOut: expect.any(String),
        price: expect.any(String)
      });
    });

    it('should validate request parameters', async () => {
      const response = await request(app)
        .post('/connectors/mydex/router/quote-swap')
        .send({
          chain: 'ethereum',
          network: 'mainnet'
          // Missing required fields
        });

      expect(response.status).toBe(400);
      expect(response.body.error).toContain('validation');
    });
  });

  describe('POST /connectors/mydex/router/execute-swap', () => {
    it('should execute swap transaction', async () => {
      const response = await request(app)
        .post('/connectors/mydex/router/execute-swap')
        .send({
          chain: 'ethereum',
          network: 'mainnet',
          connector: 'mydex',
          address: '0x...',
          base: 'USDC',
          quote: 'WETH',
          amount: '1000000',
          side: 'SELL',
          slippage: 0.01
        });

      expect(response.status).toBe(200);
      expect(response.body.txHash).toMatch(/^0x/);
    });
  });
});

WebSocket Testing

describe('WebSocket connections', () => {
  let ws: WebSocket;

  beforeEach((done) => {
    ws = new WebSocket('ws://localhost:15888/ws');
    ws.on('open', done);
  });

  afterEach(() => {
    ws.close();
  });

  it('should stream price updates', (done) => {
    ws.send(JSON.stringify({
      type: 'subscribe',
      channel: 'prices',
      params: {
        connector: 'mydex',
        pairs: ['USDC-WETH']
      }
    }));

    ws.on('message', (data) => {
      const message = JSON.parse(data);
      expect(message.type).toBe('price_update');
      expect(message.data.pair).toBe('USDC-WETH');
      expect(message.data.price).toBeGreaterThan(0);
      done();
    });
  });
});

Mock Creation

SDK Mocking

export class MockSDK {
  private liquidity = 1000000;
  private priceImpact = 0.01;

  setLiquidity(amount: number): void {
    this.liquidity = amount;
  }

  setPriceImpact(impact: number): void {
    this.priceImpact = impact;
  }

  async getQuote(params: QuoteParams): Promise<Quote> {
    if (this.liquidity === 0) {
      throw new Error('Insufficient liquidity');
    }

    return {
      amountOut: this.calculateOutput(params.amountIn),
      priceImpact: this.priceImpact,
      route: [{ pool: '0x...', percentage: 100 }]
    };
  }

  async executeSwap(params: SwapParams): Promise<Transaction> {
    return {
      hash: `0x${Math.random().toString(16).slice(2)}`,
      gasUsed: BigNumber.from('100000'),
      status: 'success',
      blockNumber: 12345678
    };
  }
}

Response Mocking

export const mockResponses = {
  tokens: {
    USDC: {
      address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
      decimals: 6,
      symbol: 'USDC',
      name: 'USD Coin'
    },
    WETH: {
      address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
      decimals: 18,
      symbol: 'WETH',
      name: 'Wrapped Ether'
    }
  },

  pools: {
    USDC_WETH: {
      address: '0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8',
      token0: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
      token1: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
      fee: 3000,
      liquidity: '1000000000000000000',
      sqrtPriceX96: '1234567890123456789012345678901234'
    }
  }
};

Performance Testing

Load Testing

describe('Performance', () => {
  it('should handle concurrent requests', async () => {
    const requests = Array(100).fill(null).map(() =>
      connector.quote({
        base: fixtures.tokens.USDC,
        quote: fixtures.tokens.WETH,
        amount: '1000000',
        side: 'SELL'
      })
    );

    const start = Date.now();
    const results = await Promise.all(requests);
    const duration = Date.now() - start;

    expect(results).toHaveLength(100);
    expect(duration).toBeLessThan(5000); // 5 seconds for 100 requests
  });

  it('should cache appropriately', async () => {
    const spy = jest.spyOn(mockSDK, 'getPool');

    // First call
    await connector.getPoolInfo('USDC-WETH');
    expect(spy).toHaveBeenCalledTimes(1);

    // Second call should use cache
    await connector.getPoolInfo('USDC-WETH');
    expect(spy).toHaveBeenCalledTimes(1);

    // After cache expiry
    jest.advanceTimersByTime(31000);
    await connector.getPoolInfo('USDC-WETH');
    expect(spy).toHaveBeenCalledTimes(2);
  });
});

Memory Testing

describe('Memory management', () => {
  it('should not leak memory on repeated operations', async () => {
    const initialMemory = process.memoryUsage().heapUsed;

    for (let i = 0; i < 1000; i++) {
      await connector.quote({
        base: fixtures.tokens.USDC,
        quote: fixtures.tokens.WETH,
        amount: '1000000',
        side: 'SELL'
      });
    }

    global.gc(); // Force garbage collection
    const finalMemory = process.memoryUsage().heapUsed;
    const memoryIncrease = finalMemory - initialMemory;

    expect(memoryIncrease).toBeLessThan(10 * 1024 * 1024); // Less than 10MB
  });
});

Error Testing

Error Scenarios

describe('Error handling', () => {
  it('should handle network errors gracefully', async () => {
    mockSDK.simulateNetworkError();

    await expect(connector.quote({
      base: fixtures.tokens.USDC,
      quote: fixtures.tokens.WETH,
      amount: '1000000',
      side: 'SELL'
    })).rejects.toThrow('Network error');
  });

  it('should handle invalid token addresses', async () => {
    await expect(connector.quote({
      base: { ...fixtures.tokens.USDC, address: 'invalid' },
      quote: fixtures.tokens.WETH,
      amount: '1000000',
      side: 'SELL'
    })).rejects.toThrow('Invalid token address');
  });

  it('should handle transaction failures', async () => {
    mockSDK.simulateTransactionFailure();

    await expect(connector.trade({
      wallet: fixtures.wallet,
      base: fixtures.tokens.USDC,
      quote: fixtures.tokens.WETH,
      amount: '1000000',
      side: 'SELL',
      slippage: 0.01
    })).rejects.toThrow('Transaction failed');
  });

  it('should handle slippage exceeded', async () => {
    mockSDK.setPriceImpact(0.1);

    await expect(connector.trade({
      wallet: fixtures.wallet,
      base: fixtures.tokens.USDC,
      quote: fixtures.tokens.WETH,
      amount: '1000000',
      side: 'SELL',
      slippage: 0.01
    })).rejects.toThrow('Slippage exceeded');
  });
});

Test Coverage

Running Coverage Reports

# Generate coverage report
pnpm test:coverage

# Generate HTML report
pnpm test:coverage -- --coverageReporters=html

# Check coverage thresholds
pnpm test:coverage -- --coverageThreshold='{
  "global": {
    "branches": 75,
    "functions": 75,
    "lines": 75,
    "statements": 75
  }
}'

Coverage Configuration

// jest.config.js
module.exports = {
  collectCoverage: true,
  collectCoverageFrom: [
    'src/connectors/mydex/**/*.ts',
    '!src/connectors/mydex/**/*.test.ts',
    '!src/connectors/mydex/types.ts'
  ],
  coverageThreshold: {
    global: {
      branches: 75,
      functions: 75,
      lines: 75,
      statements: 75
    }
  },
  coverageReporters: ['text', 'lcov', 'html']
};

Test Utilities

Helper Functions

// test/utils/test-helpers.ts
export function createMockWallet(address?: string): Wallet {
  return {
    address: address || '0x' + '0'.repeat(40),
    privateKey: '0x' + '0'.repeat(64),
    signTransaction: jest.fn(),
    signMessage: jest.fn()
  };
}

export function createMockToken(
  symbol: string,
  decimals: number = 18
): Token {
  return {
    symbol,
    decimals,
    address: `0x${symbol}${'0'.repeat(40 - symbol.length)}`,
    name: `Mock ${symbol}`
  };
}

export async function waitForEvent(
  emitter: EventEmitter,
  event: string,
  timeout: number = 5000
): Promise<any> {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => {
      reject(new Error(`Timeout waiting for event: ${event}`));
    }, timeout);

    emitter.once(event, (data) => {
      clearTimeout(timer);
      resolve(data);
    });
  });
}

Best Practices

1. Test Independence

Each test should be independent and not rely on others:

// Good
beforeEach(() => {
  connector = new MyDex('ethereum', 'mainnet');
});

// Bad - relies on previous test state
it('test 1', () => {
  connector.setState('value');
});

it('test 2', () => {
  expect(connector.getState()).toBe('value'); // Depends on test 1
});

2. Descriptive Test Names

Use clear, descriptive test names:

// Good
it('should return USDC-WETH pool info with correct reserves and fee tier');

// Bad
it('works');

3. Test Data Builders

Create builders for complex test data:

class PoolBuilder {
  private pool = { ...defaultPool };

  withTokens(token0: string, token1: string): this {
    this.pool.token0 = token0;
    this.pool.token1 = token1;
    return this;
  }

  withLiquidity(amount: string): this {
    this.pool.liquidity = amount;
    return this;
  }

  build(): Pool {
    return this.pool;
  }
}

// Usage
const pool = new PoolBuilder()
  .withTokens('USDC', 'WETH')
  .withLiquidity('1000000')
  .build();

4. Async Testing

Always handle async operations properly:

// Good
it('should handle async operation', async () => {
  const result = await asyncOperation();
  expect(result).toBeDefined();
});

// Bad - might pass before async completes
it('should handle async operation', () => {
  asyncOperation().then(result => {
    expect(result).toBeDefined();
  });
});

Resources