Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Expensify/App/llms.txt

Use this file to discover all available pages before exploring further.

Testing Philosophy

New Expensify follows a comprehensive testing strategy:
  • Unit Tests: Test individual components and functions
  • Integration Tests: Test feature workflows
  • Performance Tests: Catch performance regressions
  • Type Safety: TypeScript for compile-time checks

Running Tests

All Tests

# Run all tests
npm test

# Run tests in watch mode
npm test -- --watch

# Run specific test file
npm test -- MyComponent.test.tsx

# Run tests matching pattern
npm test -- --testNamePattern="renders correctly"

Test Coverage

# Generate coverage report
npm test -- --coverage

# View in browser
open coverage/lcov-report/index.html

Writing Unit Tests

Component Tests

import {render, fireEvent, screen} from '@testing-library/react-native';
import Onyx from 'react-native-onyx';
import Button from '@components/Button';
import ONYXKEYS from '@src/ONYXKEYS';

describe('Button', () => {
  beforeAll(() => {
    Onyx.init({keys: ONYXKEYS});
  });

  afterEach(() => {
    Onyx.clear();
  });

  it('renders with text', () => {
    render(<Button text="Click Me" />);
    expect(screen.getByText('Click Me')).toBeTruthy();
  });

  it('calls onPress when pressed', () => {
    const onPress = jest.fn();
    render(<Button text="Click Me" onPress={onPress} />);
    
    fireEvent.press(screen.getByText('Click Me'));
    expect(onPress).toHaveBeenCalledTimes(1);
  });

  it('is disabled when disabled prop is true', () => {
    const onPress = jest.fn();
    render(<Button text="Click Me" isDisabled onPress={onPress} />);
    
    fireEvent.press(screen.getByText('Click Me'));
    expect(onPress).not.toHaveBeenCalled();
  });
});

Testing with Onyx

import waitForBatchedUpdates from '@libs/waitForBatchedUpdates';
import * as TestHelper from '@tests/unit/TestHelper';

describe('ReportScreen', () => {
  beforeAll(() => {
    Onyx.init({keys: ONYXKEYS});
  });

  beforeEach(() => {
    // Set up test data
    return Onyx.merge(ONYXKEYS.SESSION, {
      authToken: 'test-token',
      accountID: 1,
      email: 'test@example.com',
    });
  });

  afterEach(() => {
    Onyx.clear();
  });

  it('displays report name', async () => {
    const reportID = '123';
    await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {
      reportID,
      reportName: 'Test Report',
    });

    render(<ReportScreen reportID={reportID} />);
    
    await waitForBatchedUpdates();
    expect(screen.getByText('Test Report')).toBeTruthy();
  });
});

Testing Hooks

import {renderHook} from '@testing-library/react-hooks';
import {useReportActions} from '@hooks/useReportActions';

describe('useReportActions', () => {
  it('returns sorted actions', async () => {
    const reportID = '123';
    await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, {
      '1': {created: '2024-01-01', message: 'First'},
      '2': {created: '2024-01-02', message: 'Second'},
    });

    const {result} = renderHook(() => useReportActions(reportID));
    
    await waitForBatchedUpdates();
    expect(result.current).toHaveLength(2);
    expect(result.current[0].message).toBe('First');
  });
});

Mocking API Calls

import * as API from '@libs/API';

jest.mock('@libs/API');

describe('createWorkspace', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('calls API with correct parameters', () => {
    const mockWrite = jest.spyOn(API, 'write');
    
    createWorkspace('Test Workspace');
    
    expect(mockWrite).toHaveBeenCalledWith(
      'CreateWorkspace',
      {policyName: 'Test Workspace'},
      expect.objectContaining({
        optimisticData: expect.any(Array),
      }),
    );
  });
});

Testing Utilities

Custom Render with Providers

import {render} from '@testing-library/react-native';
import {LocaleContextProvider} from '@components/LocaleContextProvider';
import ThemeProvider from '@components/ThemeProvider';

function renderWithProviders(component: React.ReactElement) {
  return render(
    <ThemeProvider>
      <LocaleContextProvider>
        {component}
      </LocaleContextProvider>
    </ThemeProvider>
  );
}

// Usage
test('renders with providers', () => {
  renderWithProviders(<MyComponent />);
});

Waiting for Updates

import waitForBatchedUpdates from '@libs/waitForBatchedUpdates';
import {waitFor} from '@testing-library/react-native';

// Wait for Onyx updates
await waitForBatchedUpdates();

// Wait for specific condition
await waitFor(() => {
  expect(screen.getByText('Loaded')).toBeTruthy();
});

Performance Tests

New Expensify uses Reassure for performance testing.
import {measurePerformance} from 'reassure';
import MyComponent from '../MyComponent';

test('MyComponent renders efficiently', async () => {
  await measurePerformance(<MyComponent data={mockData} />);
});

Running Performance Tests

# Run performance tests
npm run test:perf

# Compare with baseline
npm run test:perf -- --compare
See REASSURE_PERFORMANCE_TEST.md for details.

Testing Best Practices

1. Test Behavior, Not Implementation

// ✅ Good: Test what user sees
test('displays error message', () => {
  render(<LoginForm />);
  fireEvent.changeText(screen.getByLabelText('Email'), 'invalid');
  fireEvent.press(screen.getByText('Submit'));
  
  expect(screen.getByText('Invalid email')).toBeTruthy();
});

// ❌ Bad: Test internal state
test('sets error state', () => {
  const component = render(<LoginForm />);
  expect(component.instance().state.error).toBe(true);
});

2. Keep Tests Independent

// ✅ Good: Each test sets up its own data
describe('ReportList', () => {
  it('shows empty state', async () => {
    await Onyx.merge(ONYXKEYS.COLLECTION.REPORT, {});
    render(<ReportList />);
    expect(screen.getByText('No reports')).toBeTruthy();
  });

  it('shows reports', async () => {
    await Onyx.merge(ONYXKEYS.COLLECTION.REPORT, {
      '1': {reportID: '1', reportName: 'Report 1'},
    });
    render(<ReportList />);
    expect(screen.getByText('Report 1')).toBeTruthy();
  });
});

3. Use Descriptive Test Names

// ✅ Good: Clear what is being tested
it('displays error when email is invalid', () => {});
it('disables submit button when form is submitting', () => {});
it('navigates to home screen after successful login', () => {});

// ❌ Bad: Vague test names
it('works correctly', () => {});
it('test 1', () => {});

4. Clean Up After Tests

describe('MyComponent', () => {
  afterEach(() => {
    jest.clearAllMocks();
    Onyx.clear();
  });

  // Tests...
});

Testing Checklist

Before submitting a PR, ensure:
  • All existing tests pass
  • New features have tests
  • Bug fixes have regression tests
  • Tests are independent and repeatable
  • No console errors or warnings
  • Performance tests pass (if applicable)

Common Testing Patterns

Testing Async Operations

it('loads data asynchronously', async () => {
  render(<DataComponent />);
  
  expect(screen.getByText('Loading...')).toBeTruthy();
  
  await waitFor(() => {
    expect(screen.getByText('Data loaded')).toBeTruthy();
  });
});

Testing Navigation

import Navigation from '@libs/Navigation/Navigation';

jest.mock('@libs/Navigation/Navigation');

it('navigates to settings on button press', () => {
  render(<MyComponent />);
  fireEvent.press(screen.getByText('Settings'));
  
  expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.SETTINGS);
});

Testing Forms

it('submits form with valid data', () => {
  const onSubmit = jest.fn();
  render(<MyForm onSubmit={onSubmit} />);
  
  fireEvent.changeText(screen.getByLabelText('Name'), 'John');
  fireEvent.changeText(screen.getByLabelText('Email'), 'john@example.com');
  fireEvent.press(screen.getByText('Submit'));
  
  expect(onSubmit).toHaveBeenCalledWith({
    name: 'John',
    email: 'john@example.com',
  });
});

Debugging Tests

View Component Output

import {screen, debug} from '@testing-library/react-native';

test('debugging test', () => {
  render(<MyComponent />);
  
  // Print component tree
  debug();
  
  // Print specific element
  debug(screen.getByText('Hello'));
});

Run Single Test

# Run specific test file
npm test -- MyComponent.test.tsx

# Run specific test case
npm test -- --testNamePattern="renders correctly"

# Run in watch mode for debugging
npm test -- --watch MyComponent.test.tsx

Next Steps

Pull Requests

Submit PRs with tests

Coding Standards

Follow code quality standards

Architecture

Understand what to test

Performance Tests

Performance testing guide