Skip to content

Latest commit

 

History

History
407 lines (307 loc) · 11.1 KB

File metadata and controls

407 lines (307 loc) · 11.1 KB

E2E API Mocking Guide

This guide explains how to mock APIs in MetaMask Mobile E2E tests. The mocking system allows tests to run predictably by intercepting HTTP requests and returning controlled responses.

Architecture Overview

The E2E mocking system consists of three main components:

  1. Default Mocks (tests/api-mocking/mock-responses/defaults/) - Shared mocks used across all tests
  2. Test-Specific Mocks - Custom mocks defined within individual test files

How Mocking Works

  • All network requests go through a proxy server that can intercept and mock responses
  • Default mocks are automatically loaded for all tests via FixtureHelper.createMockAPIServer() -> startMockSerer()
  • Test-specific mocks take precedence over default mocks
  • The mock server runs on a dedicated port and is automatically started/stopped by the test framework

Default Mocks

Default mocks are organized by API category in tests/api-mocking/mock-responses/defaults/:

defaults/
├── index.ts              # Aggregates all default mocks
├── accounts.ts           # Account-related API mocks
├── defi-adapter.ts       # DeFi protocol mocks
├── dapp-scanning.ts      # Dapp security scanning mocks
├── metametrics-test.ts   # Analytics mocks for testing
├── onramp-apis.ts        # Onramp service mocks
├── price-apis.ts         # Price feed mocks
├── staking.ts            # Staking API mocks
├── swap-apis.ts          # Swap/exchange API mocks
├── token-apis.ts         # Token metadata API mocks
├── user-storage.ts       # User storage service mocks
├── walletconnect.ts      # WalletConnect mocks
└── web-3-auth.ts         # Web3 authentication mocks

Adding New Default Mocks

To add default mocks that all tests can benefit from:

  1. Create or edit a category file in defaults/ folder:
// defaults/my-new-service.ts
import { MockApiEndpoint } from '../../framework/types';

export const MY_SERVICE_MOCKS = {
  GET: [
    {
      urlEndpoint: 'https://api.myservice.com/data',
      responseCode: 200,
      response: { success: true, data: [] },
    },
  ] as MockApiEndpoint[],
  POST: [
    {
      urlEndpoint: 'https://api.myservice.com/submit',
      responseCode: 201,
      response: { id: '123', status: 'created' },
    },
  ] as MockApiEndpoint[],
};
  1. Add to the main index file:
// defaults/index.ts
import { MY_SERVICE_MOCKS } from './my-new-service';

export const DEFAULT_MOCKS = {
  GET: [
    // ... existing mocks
    ...(MY_SERVICE_MOCKS.GET || []),
  ],
  POST: [
    // ... existing mocks
    ...(MY_SERVICE_MOCKS.POST || []),
  ],
  // ... other methods
};

Default mocks are automatically included in all tests through FixtureHelper.createMockAPIServer().

Test-Specific Mocks

Test-specific mocks are defined within individual test files and take precedence over default mocks.

Method 1: Using testSpecificMock Parameter

Pass a testSpecificMock function to withFixtures:

import { withFixtures } from '../framework/fixtures/FixtureHelper';
import { setupMockRequest } from '../api-mocking/mockHelpers';

describe('My Test Suite', () => {
  it('should handle custom API response', async () => {
    const testSpecificMock = async (mockServer: Mockttp) => {
      await setupMockRequest(mockServer, {
        requestMethod: 'GET',
        url: 'https://api.example.com/custom',
        response: { customData: 'test' },
        responseCode: 200,
      });
    };

    await withFixtures(
      {
        fixture: new FixtureBuilder().build(),
        testSpecificMock,
      },
      async ({ mockServer }) => {
        // Your test code here
      },
    );
  });
});

Method 2: Using Mock Server Reference

Access the mock server directly within the test:

await withFixtures(
  {
    fixture: new FixtureBuilder().build(),
  },
  async ({ mockServer }) => {
    // Set up additional mocks within the test
    await setupMockRequest(mockServer, {
      requestMethod: 'POST',
      url: 'https://api.example.com/submit',
      response: { result: 'success' },
      responseCode: 201,
    });

    // Your test code here
  },
);

Mock Helper Functions

The tests/api-mocking/mockHelpers.ts file provides several utilities for mocking:

setupMockRequest

For simple GET/POST/PUT/DELETE requests:

await setupMockRequest(mockServer, {
  requestMethod: 'GET',
  url: 'https://api.example.com/data',
  response: { data: [] },
  responseCode: 200,
});

setupMockPostRequest

For POST requests with body validation:

await setupMockPostRequest(
  mockServer,
  'https://api.example.com/validate',
  { field: 'expectedValue' }, // Expected request body
  { result: 'validated' }, // Response
  {
    statusCode: 200,
    ignoreFields: ['timestamp'], // Fields to ignore in body validation
  },
);

FixtureHelper Integration

Automatic Mock Server Setup

FixtureHelper.createMockAPIServer() automatically:

  1. Starts a mock server on the configured port
  2. Loads all default mocks from defaults/index.ts
  3. Applies test-specific mocks (if provided)
  4. Calls mockNotificationServices() for notification-related mocks
  5. Calls setupRemoteFeatureFlagsMock() for default remote feature flags mocks

Notification Services Mocking

The mockNotificationServices() function in FixtureHelper.ts automatically sets up mocks for:

  • Push notification APIs
  • Notification list/read/update endpoints
  • Feature announcement APIs
  • Authentication services for notifications

This is applied to all tests automatically, so no additional setup is needed for basic notification functionality.

Best Practices

1. Use Default Mocks for Common APIs

If multiple tests need the same API mocked, add it to the appropriate default mock file rather than duplicating it in each test.

2. Be Specific with URL Matching

Use specific URL patterns to avoid unintended matches:

// Good - specific endpoint
urlEndpoint: 'https://api.metamask.io/prices/eth';

// Better - use regex for dynamic parts
urlEndpoint: /^https:\/\/api\.metamask\.io\/prices\/[a-z]+$/;

// Avoid - too broad
urlEndpoint: 'metamask.io';

3. Handle POST Request Bodies Properly

For POST requests, validate the request body when needed:

await setupMockPostRequest(
  mockServer,
  'https://api.example.com/submit',
  {
    method: 'transfer',
    amount: '1000000000000000000', // Expected request body
  },
  { success: true },
  {
    ignoreFields: ['timestamp', 'nonce'], // Ignore dynamic fields
  },
);

4. Use Descriptive Response Codes

Always specify appropriate HTTP response codes:

// Success cases
responseCode: 200, // OK
responseCode: 201, // Created
responseCode: 204, // No Content

// Error cases
responseCode: 400, // Bad Request
responseCode: 404, // Not Found
responseCode: 500, // Internal Server Error

5. Organize Test-Specific Mocks

For complex tests with many mocks, organize them in a setup function:

const setupTestMocks = async (mockServer: Mockttp) => {
  // Price API mocks
  await setupMockRequest(mockServer, {
    /* ... */
  });

  // Token API mocks
  await setupMockRequest(mockServer, {
    /* ... */
  });

  // Swap API mocks
  await setupMockRequest(mockServer, {
    /* ... */
  });
};

// Use in test
await withFixtures(
  {
    fixture: new FixtureBuilder().build(),
    testSpecificMock: setupTestMocks,
  },
  async ({ mockServer }) => {
    // Test code
  },
);

Remote Feature Flags Mocking

MetaMask Mobile uses remote feature flags to control features dynamically. The E2E framework provides specialized helpers for mocking these flags.

setupRemoteFeatureFlagsMock Helper

Use setupRemoteFeatureFlagsMock from tests/api-mocking/helpers/remoteFeatureFlagsHelper to mock feature flags:

import { setupRemoteFeatureFlagsMock } from '../../api-mocking/helpers/remoteFeatureFlagsHelper';

const testSpecificMock = async (mockServer: Mockttp) => {
  await setupRemoteFeatureFlagsMock(
    mockServer,
    { rewards: true },
    'main', // distribution (optional, defaults to 'main')
  );
};

Predefined Feature Flag Mocks

Common feature flag configurations are available in tests/api-mocking/mock-responses/feature-flags-mocks.ts:

import { confirmationsRedesignedFeatureFlags } from '../../api-mocking/mock-responses/feature-flags-mocks';

// For redesigned confirmations UI
const testSpecificMock = async (mockServer: Mockttp) => {
  await setupRemoteFeatureFlagsMock(
    mockServer,
    Object.assign({}, ...confirmationsRedesignedFeatureFlags),
  );
};

Feature Flag Override Patterns

The helper supports both simple and nested flag overrides:

// Simple boolean flags
await setupRemoteFeatureFlagsMock(mockServer, {
  rewards: true,
  carouselBanners: false,
});

// Nested object flags (deep merge support)
await setupRemoteFeatureFlagsMock(mockServer, {
  bridgeConfig: {
    support: true,
    refreshRate: 5000,
  },
});

// Combining predefined configs with overrides
await setupRemoteFeatureFlagsMock(mockServer, {
  ...Object.assign({}, ...confirmationsRedesignedFeatureFlags),
  rewards: true, // Override specific flags
});

Distribution and Environment Support

Feature flags can vary by distribution (main/flask) and environment (dev/prod):

// Flask distribution with custom flags
await setupRemoteFeatureFlagsMock(mockServer, { perpsEnabled: true }, 'flask');

// The helper automatically handles both dev and prod environment URLs

Best Practices for Feature Flags

  1. Use Object.assign with arrays: When combining multiple feature flag arrays, use Object.assign({}, ...arrays) to properly merge objects:
// Correct - properly merges flag objects
Object.assign({}, ...confirmationsRedesignedFeatureFlags)

// Incorrect - spreads array items directly
...confirmationsRedesignedFeatureFlags
  1. Prefer predefined configurations: Use existing configurations from feature-flags-mocks.ts when possible rather than defining flags inline.

  2. Test both flag states: Create tests for both enabled and disabled feature flag states when the feature behavior differs significantly.

  3. Use descriptive test names: Include feature flag state in test descriptions when relevant.

Debugging Mocks

Live Request Validation

The mock server tracks requests that weren't mocked and logs them at the end of tests. Check the test output for warnings about unmocked requests. (This will soon be enforced to track new untracked request)

Enable Debug Logging

Add debug logging to see which mocks are being hit:

// The mock helpers automatically log when mocks are triggered
// Check test output for lines like:
// "Mocking GET request to: https://api.example.com/data"

Common Issues

  1. Mock not triggering: Check URL pattern matching
  2. Wrong response: Verify mock takes precedence (test-specific > default)
  3. POST body validation failing: Check ignoreFields and expected request body format
  4. Feature flags not applying: Ensure you're using Object.assign({}, ...arrays) when combining flag arrays