Skip to main content

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: '[email protected]',
    });
  });

  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'), '[email protected]');
  fireEvent.press(screen.getByText('Submit'));
  
  expect(onSubmit).toHaveBeenCalledWith({
    name: 'John',
    email: '[email protected]',
  });
});

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

Build docs developers (and LLMs) love