Frontend Testing Interview Guide: Jest, React Testing Library & Cypress

·15 min read
testingjestreact-testing-librarycypressfrontendinterview-preparation

Testing is where junior developers become professionals. Anyone can write code that works once—testing proves it keeps working.

Frontend testing has evolved beyond "just check if it renders." Modern interviews expect you to understand the testing pyramid, component testing patterns, and when to use different testing strategies.

This guide covers Jest, React Testing Library, and Cypress—the testing stack most companies use and most interviews ask about.


Testing Fundamentals

Before writing tests, understand what you're testing and why.

The Testing Pyramid

        /\
       /  \         E2E Tests
      /    \        (few, slow, expensive)
     /------\
    /        \      Integration Tests
   /          \     (some, moderate speed)
  /------------\
 /              \   Unit Tests
/________________\  (many, fast, cheap)
LevelWhat It TestsSpeedConfidenceMaintenance
UnitSingle function/component in isolationFastLowerLow
IntegrationMultiple units working togetherMediumMediumMedium
E2EFull user flows in real browserSlowHighestHigh

The pyramid principle: More tests at the bottom, fewer at the top. Unit tests are fast and pinpoint failures. E2E tests are slow but catch integration issues.

What to Test

Do test:

  • User interactions (clicks, form submissions)
  • Conditional rendering
  • Data transformations
  • Error states
  • Edge cases (empty lists, loading states)

Don't test:

  • Implementation details (internal state, private methods)
  • Third-party libraries
  • Styling (unless critical)
  • Things the framework already tests

Testing Philosophy

React Testing Library's guiding principle:

"The more your tests resemble the way your software is used, the more confidence they can give you."

This means:

  • Query elements like users find them (by text, role, label)
  • Test behavior, not implementation
  • Avoid testing internal state directly

Jest Essentials

Jest is the standard JavaScript test runner. It handles test execution, assertions, mocking, and coverage.

Basic Test Structure

// sum.test.js
import { sum, multiply } from './math';
 
describe('math utilities', () => {
  describe('sum', () => {
    it('adds two positive numbers', () => {
      expect(sum(1, 2)).toBe(3);
    });
 
    it('handles negative numbers', () => {
      expect(sum(-1, -2)).toBe(-3);
    });
 
    it('handles zero', () => {
      expect(sum(0, 5)).toBe(5);
    });
  });
 
  describe('multiply', () => {
    it('multiplies two numbers', () => {
      expect(multiply(3, 4)).toBe(12);
    });
  });
});

Common Matchers

// Equality
expect(value).toBe(3);              // Strict equality (===)
expect(value).toEqual({ a: 1 });    // Deep equality for objects
expect(value).toStrictEqual(obj);   // Deep equality + undefined checks
 
// Truthiness
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeDefined();
 
// Numbers
expect(value).toBeGreaterThan(3);
expect(value).toBeLessThanOrEqual(5);
expect(value).toBeCloseTo(0.3, 5);  // Floating point
 
// Strings
expect(string).toMatch(/pattern/);
expect(string).toContain('substring');
 
// Arrays
expect(array).toContain('item');
expect(array).toHaveLength(3);
expect(array).toEqual(expect.arrayContaining([1, 2]));
 
// Objects
expect(obj).toHaveProperty('key');
expect(obj).toHaveProperty('nested.key', 'value');
expect(obj).toMatchObject({ partial: 'match' });
 
// Exceptions
expect(() => badFunction()).toThrow();
expect(() => badFunction()).toThrow('specific message');
expect(() => badFunction()).toThrow(CustomError);

Mocking Functions

// Create a mock function
const mockFn = jest.fn();
 
// Call it
mockFn('arg1', 'arg2');
 
// Assert on calls
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(1);
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
 
// Mock return values
const mockFn = jest.fn()
  .mockReturnValue('default')
  .mockReturnValueOnce('first call')
  .mockReturnValueOnce('second call');
 
// Mock implementation
const mockFn = jest.fn((x) => x * 2);
 
// Mock resolved/rejected values (async)
const mockAsync = jest.fn()
  .mockResolvedValue({ data: 'success' })
  .mockRejectedValueOnce(new Error('fail'));

Mocking Modules

// Mock entire module
jest.mock('./api');
 
import { fetchUser } from './api';
 
// fetchUser is now a mock function
fetchUser.mockResolvedValue({ id: 1, name: 'Alice' });
 
// Partial mock (keep some real implementations)
jest.mock('./utils', () => ({
  ...jest.requireActual('./utils'),
  formatDate: jest.fn(() => '2026-01-01')
}));

Spying on Methods

// Spy on existing method
const spy = jest.spyOn(object, 'method');
 
// Spy and mock implementation
jest.spyOn(console, 'error').mockImplementation(() => {});
 
// Restore original
spy.mockRestore();

Async Testing

// Async/await
it('fetches user data', async () => {
  const user = await fetchUser(1);
  expect(user.name).toBe('Alice');
});
 
// Promises
it('fetches user data', () => {
  return fetchUser(1).then(user => {
    expect(user.name).toBe('Alice');
  });
});
 
// Testing rejections
it('handles errors', async () => {
  await expect(fetchUser(-1)).rejects.toThrow('Invalid ID');
});
 
// Fake timers
jest.useFakeTimers();
 
it('debounces input', () => {
  const callback = jest.fn();
  const debounced = debounce(callback, 500);
 
  debounced();
  expect(callback).not.toHaveBeenCalled();
 
  jest.advanceTimersByTime(500);
  expect(callback).toHaveBeenCalled();
});

Setup and Teardown

describe('database tests', () => {
  // Run once before all tests in this describe
  beforeAll(async () => {
    await db.connect();
  });
 
  // Run before each test
  beforeEach(async () => {
    await db.clear();
  });
 
  // Run after each test
  afterEach(() => {
    jest.clearAllMocks();
  });
 
  // Run once after all tests
  afterAll(async () => {
    await db.disconnect();
  });
 
  it('inserts data', async () => {
    // test code
  });
});

React Testing Library

React Testing Library (RTL) renders components and provides queries to find elements the way users would.

Basic Component Test

// Button.jsx
function Button({ onClick, children }) {
  return (
    <button onClick={onClick}>
      {children}
    </button>
  );
}
 
// Button.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Button from './Button';
 
describe('Button', () => {
  it('renders children', () => {
    render(<Button>Click me</Button>);
    expect(screen.getByText('Click me')).toBeInTheDocument();
  });
 
  it('calls onClick when clicked', async () => {
    const handleClick = jest.fn();
    render(<Button onClick={handleClick}>Click me</Button>);
 
    await userEvent.click(screen.getByText('Click me'));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });
});

Query Priority

RTL provides multiple ways to find elements. Use them in this priority order:

// 1. Accessible by Everyone
screen.getByRole('button', { name: 'Submit' })  // Best - uses accessibility
screen.getByLabelText('Email')                   // For form fields
screen.getByPlaceholderText('Enter email')       // Less preferred than label
screen.getByText('Welcome')                      // For non-interactive elements
screen.getByDisplayValue('current value')        // For filled form fields
 
// 2. Semantic Queries
screen.getByAltText('Profile picture')           // For images
screen.getByTitle('Close')                       // For title attributes
 
// 3. Test IDs (last resort)
screen.getByTestId('custom-element')             // When nothing else works

Why this order? Queries at the top ensure your app is accessible. If you can't find an element by role, your app might have accessibility issues.

Query Variants

// getBy - throws if not found (use when element should exist)
screen.getByText('Hello')
 
// queryBy - returns null if not found (use for asserting absence)
expect(screen.queryByText('Error')).not.toBeInTheDocument()
 
// findBy - returns promise, waits for element (use for async content)
await screen.findByText('Loaded data')
 
// Plural variants for multiple elements
screen.getAllByRole('listitem')
screen.queryAllByRole('button')
await screen.findAllByText(/item/)

User Events

Always prefer userEvent over fireEvent—it simulates real user behavior more accurately.

import userEvent from '@testing-library/user-event';
 
it('handles user interactions', async () => {
  const user = userEvent.setup();
  render(<Form />);
 
  // Typing
  await user.type(screen.getByLabelText('Name'), 'Alice');
 
  // Clicking
  await user.click(screen.getByRole('button', { name: 'Submit' }));
 
  // Clearing and typing
  await user.clear(screen.getByLabelText('Name'));
  await user.type(screen.getByLabelText('Name'), 'Bob');
 
  // Selecting options
  await user.selectOptions(screen.getByRole('combobox'), 'option1');
 
  // Keyboard
  await user.keyboard('{Enter}');
  await user.keyboard('{Shift>}A{/Shift}'); // Shift+A
 
  // Tab navigation
  await user.tab();
 
  // Hover
  await user.hover(screen.getByText('Tooltip trigger'));
});

Testing Async Components

// UserProfile.jsx
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
 
  useEffect(() => {
    fetchUser(userId)
      .then(setUser)
      .finally(() => setLoading(false));
  }, [userId]);
 
  if (loading) return <div>Loading...</div>;
  return <div>Hello, {user.name}</div>;
}
 
// UserProfile.test.jsx
import { render, screen } from '@testing-library/react';
import UserProfile from './UserProfile';
import { fetchUser } from './api';
 
jest.mock('./api');
 
describe('UserProfile', () => {
  it('shows loading state then user data', async () => {
    fetchUser.mockResolvedValue({ name: 'Alice' });
 
    render(<UserProfile userId={1} />);
 
    // Initially shows loading
    expect(screen.getByText('Loading...')).toBeInTheDocument();
 
    // Wait for user data
    expect(await screen.findByText('Hello, Alice')).toBeInTheDocument();
 
    // Loading is gone
    expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
  });
 
  it('handles fetch error', async () => {
    fetchUser.mockRejectedValue(new Error('Failed'));
 
    render(<UserProfile userId={1} />);
 
    expect(await screen.findByText('Error loading user')).toBeInTheDocument();
  });
});

waitFor for Complex Async

import { render, screen, waitFor } from '@testing-library/react';
 
it('updates after async action', async () => {
  render(<Counter />);
 
  await userEvent.click(screen.getByRole('button', { name: 'Increment' }));
 
  // Wait for specific condition
  await waitFor(() => {
    expect(screen.getByText('Count: 1')).toBeInTheDocument();
  });
 
  // Or with timeout
  await waitFor(
    () => expect(screen.getByText('Count: 1')).toBeInTheDocument(),
    { timeout: 3000 }
  );
});

Component Testing Patterns

Common patterns for testing React components.

Testing Props

// Greeting.jsx
function Greeting({ name, formal = false }) {
  return formal ? <p>Good day, {name}</p> : <p>Hey {name}!</p>;
}
 
// Greeting.test.jsx
describe('Greeting', () => {
  it('renders informal greeting by default', () => {
    render(<Greeting name="Alice" />);
    expect(screen.getByText('Hey Alice!')).toBeInTheDocument();
  });
 
  it('renders formal greeting when formal prop is true', () => {
    render(<Greeting name="Alice" formal />);
    expect(screen.getByText('Good day, Alice')).toBeInTheDocument();
  });
});

Testing Hooks

// useCounter.js
function useCounter(initial = 0) {
  const [count, setCount] = useState(initial);
  const increment = () => setCount(c => c + 1);
  const decrement = () => setCount(c => c - 1);
  return { count, increment, decrement };
}
 
// useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';
 
describe('useCounter', () => {
  it('starts with initial value', () => {
    const { result } = renderHook(() => useCounter(10));
    expect(result.current.count).toBe(10);
  });
 
  it('increments count', () => {
    const { result } = renderHook(() => useCounter());
 
    act(() => {
      result.current.increment();
    });
 
    expect(result.current.count).toBe(1);
  });
});

Testing Context

// ThemeContext.jsx
const ThemeContext = createContext();
 
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}
 
// ThemedButton.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ThemeProvider } from './ThemeContext';
import ThemedButton from './ThemedButton';
 
// Custom render with providers
function renderWithTheme(ui) {
  return render(
    <ThemeProvider>{ui}</ThemeProvider>
  );
}
 
describe('ThemedButton', () => {
  it('uses theme from context', () => {
    renderWithTheme(<ThemedButton />);
    expect(screen.getByRole('button')).toHaveClass('light');
  });
 
  it('toggles theme on click', async () => {
    renderWithTheme(<ThemedButton />);
 
    await userEvent.click(screen.getByRole('button'));
    expect(screen.getByRole('button')).toHaveClass('dark');
  });
});

Testing Forms

// LoginForm.jsx
function LoginForm({ onSubmit }) {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
 
  const handleSubmit = (e) => {
    e.preventDefault();
    if (!email || !password) {
      setError('All fields required');
      return;
    }
    onSubmit({ email, password });
  };
 
  return (
    <form onSubmit={handleSubmit}>
      {error && <p role="alert">{error}</p>}
      <label>
        Email
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
        />
      </label>
      <label>
        Password
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
        />
      </label>
      <button type="submit">Log in</button>
    </form>
  );
}
 
// LoginForm.test.jsx
describe('LoginForm', () => {
  it('submits form with email and password', async () => {
    const handleSubmit = jest.fn();
    render(<LoginForm onSubmit={handleSubmit} />);
 
    await userEvent.type(screen.getByLabelText('Email'), 'test@example.com');
    await userEvent.type(screen.getByLabelText('Password'), 'password123');
    await userEvent.click(screen.getByRole('button', { name: 'Log in' }));
 
    expect(handleSubmit).toHaveBeenCalledWith({
      email: 'test@example.com',
      password: 'password123'
    });
  });
 
  it('shows error when fields are empty', async () => {
    render(<LoginForm onSubmit={jest.fn()} />);
 
    await userEvent.click(screen.getByRole('button', { name: 'Log in' }));
 
    expect(screen.getByRole('alert')).toHaveTextContent('All fields required');
  });
});

Testing Error Boundaries

// ErrorBoundary.test.jsx
import { render, screen } from '@testing-library/react';
import ErrorBoundary from './ErrorBoundary';
 
// Component that throws
function BrokenComponent() {
  throw new Error('Test error');
}
 
describe('ErrorBoundary', () => {
  // Suppress console.error for cleaner test output
  beforeEach(() => {
    jest.spyOn(console, 'error').mockImplementation(() => {});
  });
 
  afterEach(() => {
    console.error.mockRestore();
  });
 
  it('renders children when no error', () => {
    render(
      <ErrorBoundary>
        <div>Content</div>
      </ErrorBoundary>
    );
 
    expect(screen.getByText('Content')).toBeInTheDocument();
  });
 
  it('renders fallback when child throws', () => {
    render(
      <ErrorBoundary fallback={<div>Something went wrong</div>}>
        <BrokenComponent />
      </ErrorBoundary>
    );
 
    expect(screen.getByText('Something went wrong')).toBeInTheDocument();
  });
});

Mocking Strategies

Effective mocking isolates components and controls test conditions.

Mocking API Calls

// Using jest.mock
jest.mock('./api');
 
import { fetchUsers } from './api';
 
beforeEach(() => {
  fetchUsers.mockResolvedValue([
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' }
  ]);
});
 
it('displays users from API', async () => {
  render(<UserList />);
  expect(await screen.findByText('Alice')).toBeInTheDocument();
  expect(screen.getByText('Bob')).toBeInTheDocument();
});

MSW (Mock Service Worker)

MSW intercepts network requests—closer to real behavior than mocking modules.

// mocks/handlers.js
import { http, HttpResponse } from 'msw';
 
export const handlers = [
  http.get('/api/users', () => {
    return HttpResponse.json([
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' }
    ]);
  }),
 
  http.post('/api/users', async ({ request }) => {
    const body = await request.json();
    return HttpResponse.json({ id: 3, ...body }, { status: 201 });
  }),
 
  http.get('/api/users/:id', ({ params }) => {
    return HttpResponse.json({ id: params.id, name: 'Alice' });
  })
];
 
// mocks/server.js
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
 
export const server = setupServer(...handlers);
 
// setupTests.js
import { server } from './mocks/server';
 
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
 
// In tests, override handlers as needed
it('handles server error', async () => {
  server.use(
    http.get('/api/users', () => {
      return HttpResponse.json({ error: 'Server error' }, { status: 500 });
    })
  );
 
  render(<UserList />);
  expect(await screen.findByText('Failed to load users')).toBeInTheDocument();
});

Mocking Timers

beforeEach(() => {
  jest.useFakeTimers();
});
 
afterEach(() => {
  jest.useRealTimers();
});
 
it('shows message after delay', async () => {
  render(<DelayedMessage delay={5000} />);
 
  expect(screen.queryByText('Hello!')).not.toBeInTheDocument();
 
  // Fast-forward time
  jest.advanceTimersByTime(5000);
 
  expect(screen.getByText('Hello!')).toBeInTheDocument();
});
 
it('debounces search input', async () => {
  const onSearch = jest.fn();
  render(<SearchInput onSearch={onSearch} debounceMs={300} />);
 
  await userEvent.type(screen.getByRole('textbox'), 'test');
 
  // Not called yet (debounced)
  expect(onSearch).not.toHaveBeenCalled();
 
  // Fast-forward debounce time
  jest.advanceTimersByTime(300);
 
  expect(onSearch).toHaveBeenCalledWith('test');
});

Mocking Browser APIs

// Mocking localStorage
const localStorageMock = {
  getItem: jest.fn(),
  setItem: jest.fn(),
  clear: jest.fn()
};
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
 
// Mocking window.matchMedia
Object.defineProperty(window, 'matchMedia', {
  value: jest.fn().mockImplementation(query => ({
    matches: query === '(prefers-color-scheme: dark)',
    media: query,
    addEventListener: jest.fn(),
    removeEventListener: jest.fn()
  }))
});
 
// Mocking IntersectionObserver
global.IntersectionObserver = class {
  constructor(callback) {
    this.callback = callback;
  }
  observe() {}
  unobserve() {}
  disconnect() {}
};
 
// Mocking ResizeObserver
global.ResizeObserver = class {
  observe() {}
  unobserve() {}
  disconnect() {}
};

End-to-End Testing

E2E tests run in real browsers and test complete user flows.

Cypress Basics

// cypress/e2e/login.cy.js
describe('Login Flow', () => {
  beforeEach(() => {
    cy.visit('/login');
  });
 
  it('logs in successfully', () => {
    cy.get('[data-testid="email"]').type('user@example.com');
    cy.get('[data-testid="password"]').type('password123');
    cy.get('button[type="submit"]').click();
 
    // Assert redirect and welcome message
    cy.url().should('include', '/dashboard');
    cy.contains('Welcome back').should('be.visible');
  });
 
  it('shows error for invalid credentials', () => {
    cy.get('[data-testid="email"]').type('wrong@example.com');
    cy.get('[data-testid="password"]').type('wrongpassword');
    cy.get('button[type="submit"]').click();
 
    cy.contains('Invalid credentials').should('be.visible');
    cy.url().should('include', '/login');
  });
});

Cypress Best Practices

// Custom commands (cypress/support/commands.js)
Cypress.Commands.add('login', (email, password) => {
  cy.session([email, password], () => {
    cy.visit('/login');
    cy.get('[data-testid="email"]').type(email);
    cy.get('[data-testid="password"]').type(password);
    cy.get('button[type="submit"]').click();
    cy.url().should('include', '/dashboard');
  });
});
 
// Using custom command
describe('Dashboard', () => {
  beforeEach(() => {
    cy.login('user@example.com', 'password123');
    cy.visit('/dashboard');
  });
 
  it('displays user data', () => {
    cy.contains('Welcome back').should('be.visible');
  });
});
 
// Intercepting API calls
it('displays data from API', () => {
  cy.intercept('GET', '/api/users', {
    fixture: 'users.json'
  }).as('getUsers');
 
  cy.visit('/users');
  cy.wait('@getUsers');
 
  cy.get('[data-testid="user-card"]').should('have.length', 3);
});

Playwright Alternative

// tests/login.spec.js
import { test, expect } from '@playwright/test';
 
test.describe('Login Flow', () => {
  test('logs in successfully', async ({ page }) => {
    await page.goto('/login');
 
    await page.getByLabel('Email').fill('user@example.com');
    await page.getByLabel('Password').fill('password123');
    await page.getByRole('button', { name: 'Log in' }).click();
 
    await expect(page).toHaveURL(/dashboard/);
    await expect(page.getByText('Welcome back')).toBeVisible();
  });
});
 
// API mocking in Playwright
test('handles API error', async ({ page }) => {
  await page.route('/api/users', route => {
    route.fulfill({
      status: 500,
      body: JSON.stringify({ error: 'Server error' })
    });
  });
 
  await page.goto('/users');
  await expect(page.getByText('Failed to load')).toBeVisible();
});

Testing Best Practices

What to Test

// DO: Test user-visible behavior
it('shows error message on invalid email', async () => {
  render(<SignupForm />);
  await userEvent.type(screen.getByLabelText('Email'), 'notanemail');
  await userEvent.click(screen.getByRole('button', { name: 'Sign up' }));
  expect(screen.getByText('Please enter a valid email')).toBeInTheDocument();
});
 
// DON'T: Test implementation details
it('sets isValid state to false', () => {
  // Testing internal state is fragile and doesn't verify user experience
});

Test Structure (AAA Pattern)

it('adds item to cart', async () => {
  // Arrange - set up the test
  const product = { id: 1, name: 'Widget', price: 10 };
  render(<ProductCard product={product} />);
 
  // Act - perform the action
  await userEvent.click(screen.getByRole('button', { name: 'Add to cart' }));
 
  // Assert - verify the result
  expect(screen.getByText('Added to cart')).toBeInTheDocument();
});

Avoiding Brittle Tests

// BRITTLE: Depends on exact text
expect(screen.getByText('You have 3 items in your cart')).toBeInTheDocument();
 
// ROBUST: Uses regex for flexible matching
expect(screen.getByText(/3 items/i)).toBeInTheDocument();
 
// BRITTLE: Depends on DOM structure
container.querySelector('div > ul > li:first-child');
 
// ROBUST: Uses accessible queries
screen.getByRole('listitem', { name: 'First item' });
 
// BRITTLE: Testing exact classnames
expect(element).toHaveClass('btn-primary-lg-active');
 
// ROBUST: Testing visible behavior
expect(element).toBeVisible();
expect(element).toBeEnabled();

Test Isolation

// Each test should be independent
describe('Counter', () => {
  // Reset state between tests
  afterEach(() => {
    jest.clearAllMocks();
  });
 
  it('starts at zero', () => {
    render(<Counter />);
    expect(screen.getByText('0')).toBeInTheDocument();
  });
 
  it('increments on click', async () => {
    render(<Counter />);
    await userEvent.click(screen.getByRole('button'));
    expect(screen.getByText('1')).toBeInTheDocument();
  });
 
  // This test doesn't depend on the previous one passing
});

Snapshot Testing Guidelines

// GOOD: Small, focused snapshots
it('renders button with correct attributes', () => {
  const { container } = render(<IconButton icon="save" />);
  expect(container.firstChild).toMatchInlineSnapshot(`
    <button
      aria-label="Save"
      class="icon-button"
    >
      <svg ... />
    </button>
  `);
});
 
// BAD: Large component snapshots
it('renders entire page', () => {
  const { container } = render(<EntireDashboard />);
  expect(container).toMatchSnapshot(); // 1000+ lines, always blindly updated
});

Quick Reference

Query priority:

  1. getByRole - accessibility first
  2. getByLabelText - form fields
  3. getByText - non-interactive elements
  4. getByTestId - last resort

Query variants:

  • getBy - element should exist (throws)
  • queryBy - test absence (returns null)
  • findBy - async content (returns promise)

User events:

  • Always use userEvent over fireEvent
  • Always await user events
  • Use userEvent.setup() for proper event simulation

Mocking:

  • jest.mock() for modules
  • jest.fn() for functions
  • MSW for API calls
  • jest.useFakeTimers() for timers

Test structure:

  • Arrange → Act → Assert
  • One assertion concept per test
  • Descriptive test names

Related Articles


What's Next?

Testing is a skill that improves with practice. Start with simple component tests, then add complexity as you get comfortable.

In interviews, demonstrate that you think about testing as part of development, not an afterthought. Explain what you'd test and why. Show you understand the trade-offs between test types.

Ready to ace your interview?

Get 550+ interview questions with detailed answers in our comprehensive PDF guides.

View PDF Guides