Testing (Unit, Integration, E2E)

1. Core Concepts

Q: What is the difference between Unit, Integration, and E2E testing?
Think about the scope and speed of execution.

Basic Implementation

// 1. Unit Test (Testing a pure function)
test('add', () => {
  expect(add(1, 2)).toBe(3);
});

// 2. Integration Test (Testing Component + Child)
test('renders user list', () => {
  render(<UserList users={['Alice', 'Bob']} />);
  expect(screen.getByText('Alice')).toBeInTheDocument();
});

// 3. E2E Test (Playwright/Cypress)
test('login flow', async ({ page }) => {
  await page.goto('/login');
  await page.fill('#email', 'user@test.com');
  await page.click('#submit');
  await expect(page).toHaveURL('/dashboard');
});

Real World Example

// In a CI/CD pipeline:
// - Run Unit Tests on every commit (Fast, < 1min)
// - Run Integration Tests on PR creation (Medium, ~5min)
// - Run E2E Tests before merging to main or nightly (Slow, ~30min)
Show Answer
  • Unit: Tests a single function or component in isolation. Fast, mocks everything.
  • Integration: Tests how multiple units work together (e.g., Parent + Child + Store). Mocks network, but not internal logic.
  • E2E (End-to-End): Tests the full application in a real browser. Slow, mocks nothing (usually).
Q: Mocha vs. Jest/Vitest?
Configuration vs. "Batteries Included".
Show Answer

Mocha: Flexible but requires manual setup. You need to install separate libraries for assertions (Chai), mocking (Sinon), and coverage (Istanbul). It is often seen in older Node.js projects.

Jest / Vitest: "Batteries Included". They come with assertions (expect), mocking, and coverage built-in. Vitest is the modern choice for Vite projects as it shares the same configuration.

Interview Tip: If asked about Mocha, say: "I am proficient with Vitest/Jest, which share similar syntax (describe/it/expect). I understand Mocha requires external assertion libraries like Chai, but the core testing concepts remain the same."

Q: Why do we prefer `getByRole` over `getByTestId` in React Testing Library?
Think about accessibility and how a real user interacts with the page.

Basic Implementation

// ❌ Bad (Implementation detail)
screen.getByTestId('submit-btn');

// ✅ Good (Accessible role)
screen.getByRole('button', { name: /submit/i });

Real World Example

// If you change a <button> to a <div onClick> (bad practice),
// getByRole('button') will fail, alerting you to the accessibility regression.
// getByTestId would still pass, hiding the bug.

test('button is accessible', () => {
  render(<CustomButton />);
  // Ensures screen readers can find it
  expect(screen.getByRole('button')).toBeEnabled();
});
Show Answer

Accessibility First: RTL philosophy is "The more your tests resemble the way your software is used, the more confidence they can give you."

getByRole('button', { name: /submit/i }) ensures the element is actually accessible as a button to screen readers. getByTestId is an implementation detail that users don't see.

Q: What is the difference between `getBy`, `queryBy`, and `findBy`?
Think about error handling and async operations.

Basic Implementation

// 1. getBy: Expect it to be there NOW
const title = screen.getByText('Welcome');

// 2. queryBy: Check if it is NOT there
const error = screen.queryByText('Error');
expect(error).toBeNull();

// 3. findBy: Wait for it to appear (Async)
const data = await screen.findByText('Loaded Data');

Real World Example

test('modal opens and closes', async () => {
  // Modal is closed initially
  expect(screen.queryByRole('dialog')).not.toBeInTheDocument();

  // Open modal
  await userEvent.click(screen.getByRole('button', { name: /open/i }));
  
  // Wait for animation/render
  const modal = await screen.findByRole('dialog');
  expect(modal).toBeVisible();
});
Show Answer
  • getBy: Returns the element or throws an error if not found. Use when you expect the element to be there.
  • queryBy: Returns the element or null. Use to assert that something is NOT there.
  • findBy: Returns a Promise. Retries until the element is found or times out. Use for async updates (API calls).
Q: What is TDD (Test Driven Development)?
Red, Green, Refactor.

Basic Implementation

// 1. RED: Write test for non-existent function
test('sum', () => {
  expect(sum(2, 3)).toBe(5); // Fails: sum is not defined
});

// 2. GREEN: Write minimal code to pass
const sum = (a, b) => a + b;

// 3. REFACTOR: Improve code (if needed)
const sum = (a: number, b: number) => a + b;

Real World Example

// TDD is great for utility libraries or complex algorithms.
// Example: Building a currency formatter.
// 1. Test: format(1000, 'USD') -> '$1,000.00'
// 2. Implement basic logic.
// 3. Test: format(1000, 'EUR') -> '€1,000.00'
// 4. Update logic to handle locales.
Show Answer

A development process where you write the test before the code.

  1. Red: Write a failing test.
  2. Green: Write the minimal code to pass the test.
  3. Refactor: Clean up the code while keeping tests green.
Q: How do you test a component that makes an API call in `useEffect`?
You shouldn't hit the real API.

Basic Implementation

// Component
useEffect(() => {
  fetch('/api/user').then(res => res.json()).then(setData);
}, []);

// Test
test('loads user data', async () => {
  // Mock fetch
  global.fetch = vi.fn(() => Promise.resolve({
    json: () => Promise.resolve({ name: 'John' })
  }));

  render(<UserProfile />);
  
  // Wait for data
  expect(await screen.findByText('John')).toBeInTheDocument();
});

Real World Example

// Using MSW (Mock Service Worker) - Preferred
import { server } from './mocks/server';
import { rest } from 'msw';

test('handles server error', async () => {
  server.use(
    rest.get('/api/user', (req, res, ctx) => {
      return res(ctx.status(500));
    })
  );

  render(<UserProfile />);
  expect(await screen.findByText(/error loading/i)).toBeInTheDocument();
});
Show Answer

You must Mock the network request.

  1. Use MSW (Mock Service Worker) to intercept the request at the network layer.
  2. Or use vi.spyOn(global, 'fetch') to mock the fetch function.
  3. Use await screen.findByText(...) to wait for the UI to update after the promise resolves.

2. Advanced Scenarios

Q: What is Snapshot Testing and when is it dangerous?
It captures the rendered output.

Basic Implementation

test('renders correctly', () => {
  const { container } = render(<Button label="Click me" />);
  expect(container).toMatchSnapshot();
});
// Creates a .snap file with the HTML structure

Real World Example

// Dangerous Scenario:
// You change a class name in a global style.
// 50 snapshots fail.
// You run `jest -u` (update snapshots) without looking.
// You accidentally broke the layout, but the test now passes with the broken layout.
//
// Use snapshots for small, stable components (like Icons or UI primitives).
Show Answer

It saves a serialized string of your DOM to a file. If the DOM changes, the test fails.

Danger: It's easy to ignore. Developers often just press "u" (update snapshot) without checking if the change was intentional, making the test useless.

Q: How do you test a custom hook?
You can't call a hook outside a component.

Basic Implementation

import { renderHook, act } from '@testing-library/react';

test('useCounter increments', () => {
  const { result } = renderHook(() => useCounter(0));

  expect(result.current.count).toBe(0);

  act(() => {
    result.current.increment();
  });

  expect(result.current.count).toBe(1);
});

Real World Example

// Testing a hook that depends on Context
const wrapper = ({ children }) => (
  <AuthProvider>{children}</AuthProvider>
);

const { result } = renderHook(() => useCurrentUser(), { wrapper });
expect(result.current.isLoggedIn).toBe(false);
Show Answer

Use renderHook from @testing-library/react.

const { result } = renderHook(() => useCounter());
act(() => {
  result.current.increment();
});
expect(result.current.count).toBe(1);
Q: What is the `act()` warning in React testing?
"An update to Component inside a test was not wrapped in act..."

Basic Implementation

// ❌ Causes Warning
fireEvent.click(button); // Triggers async state update
// Test ends immediately, state updates after test finishes

// ✅ Fix
await userEvent.click(button); // userEvent wraps interactions in act() automatically
// OR
await waitFor(() => expect(screen.getByText('Updated')).toBeInTheDocument());

Real World Example

// Common in useEffect fetching
useEffect(() => {
  fetchData().then(setState);
}, []);

// If you don't wait for the fetch to finish in the test, 
// React tries to update state after the test tears down the component.
// Fix:
await screen.findByText('Data Loaded'); // Waits for the update
Show Answer

It means a state update happened (usually async) after your test finished assertions, or outside of React's call stack.

Fix: Use await waitFor(...) or findBy... to ensure the test waits for the update to finish before exiting.

Q: How do you test a component wrapped in a Context Provider (e.g., Theme, Redux)?
The component will crash if rendered in isolation.

Basic Implementation

// ❌ Fails: Could not find "store" in context
render(<ConnectedComponent />);

// ✅ Works: Wrap manually
render(
  <Provider store={store}>
    <ConnectedComponent />
  </Provider>
);

Real World Example

// Create a custom render function (test-utils.js)
const customRender = (ui, options) =>
  render(ui, { wrapper: AllTheProviders, ...options });

// In tests:
import { render } from './test-utils';
render(<ConnectedComponent />); // Automatically wrapped
Show Answer

You must wrap the component in the test with the same Provider.

Best Practice: Create a custom render function that automatically wraps everything in your global providers.

Q: What is "Code Coverage" and is 100% coverage a good goal?
Quality vs Quantity.

Basic Implementation

// Running coverage
npm test -- --coverage

// Output:
// Statements: 80%
// Branches:   70%
// Functions:  90%
// Lines:      80%

Real World Example

// 100% coverage forces you to test implementation details.
// Example: Testing a default parameter that is never used in the app.
//
// Focus on:
// 1. Critical Business Logic (Payments, Auth) -> 100%
// 2. UI Components -> Interaction tests
// 3. Utils -> Unit tests
Show Answer

It measures what percentage of your code lines were executed during tests.

Verdict: 100% is usually a waste of time (diminishing returns). Aim for ~80%, focusing on business logic and critical paths. Don't test simple getters/setters or third-party libraries.

3. Best Practices & Common Patterns

Q: Why is `userEvent` preferred over `fireEvent`?
Think about realism and browser behavior.

Basic Implementation

// ❌ fireEvent: Dispatches a DOM event directly
fireEvent.change(input, { target: { value: 'hello' } });

// ✅ userEvent: Simulates full user interaction
await userEvent.type(input, 'hello');

Real World Example

// When a real user types into an input:
// 1. Focus event fires
// 2. KeyDown event fires
// 3. Input event fires
// 4. KeyUp event fires
//
// `fireEvent.change` ONLY fires the input event.
// `userEvent.type` fires ALL of them, triggering any side effects attached to focus/blur/keys.
Show Answer

userEvent simulates full user interactions (clicks, typing) more accurately than fireEvent. It triggers all the intermediate events (focus, blur, keydown, keyup) that a real browser would, whereas fireEvent only dispatches the specific event you called.

Q: How do you test a component that relies on a Context Provider (e.g., Theme or Auth)?
The component will crash if rendered without its parent provider.

Basic Implementation

// The Component
const UserProfile = () => {
  const { user } = useAuth(); // Will throw if Provider is missing
  return <div>{user.name}</div>;
};

// The Test
test('renders user', () => {
  render(
    <AuthProvider value={{ user: { name: 'Alice' } }}>
      <UserProfile />
    </AuthProvider>
  );
  expect(screen.getByText('Alice')).toBeInTheDocument();
});

Real World Example

// Create a custom render function to avoid repetition
const customRender = (ui, options) =>
  render(ui, { wrapper: AllTheProviders, ...options });

// Now you can just use:
customRender(<UserProfile />);
Show Answer

You must wrap the component in the necessary Provider during the test. A common pattern is to create a custom render function that automatically wraps every component with your global providers (Theme, Auth, Redux).

Q: What is "Shallow Rendering" and why is it generally discouraged now?
Enzyme vs React Testing Library.

Basic Implementation

// Shallow (Enzyme style) - Conceptual
// <Parent>
//   <Child />  <-- Child is NOT rendered, just a placeholder
// </Parent>

// Full Rendering (RTL style)
// <Parent>
//   <div>Child Content</div> <-- Child is fully rendered
// </Parent>
Show Answer

Shallow Rendering: Renders a component one level deep. Child components are not rendered, only their placeholders.

Why Discouraged: It tests implementation details. If you refactor a big component into smaller sub-components, shallow tests might break even if the application works perfectly. Full rendering (Integration testing) gives more confidence that the app actually works for the user.

4. Testing Strategies

Q: How to test a form submission?
User Event.

Basic Implementation

test('submits form', async () => {
  const handleSubmit = vi.fn();
  render(<LoginForm onSubmit={handleSubmit} />);

  await userEvent.type(screen.getByLabelText(/email/i), 'test@test.com');
  await userEvent.click(screen.getByRole('button', { name: /login/i }));

  expect(handleSubmit).toHaveBeenCalledWith({ email: 'test@test.com' });
});

Real World Example

// Testing validation errors
test('shows error on invalid email', async () => {
  render(<LoginForm />);
  
  await userEvent.click(screen.getByRole('button', { name: /login/i }));
  
  expect(await screen.findByText(/email is required/i)).toBeVisible();
});
Show Answer
const user = userEvent.setup();
render(<Login />);
await user.type(screen.getByLabelText(/email/i), 'test@test.com');
await user.click(screen.getByRole('button', { name: /login/i }));
expect(handleSubmit).toHaveBeenCalledWith(...);
Q: `fireEvent` vs `userEvent`?
Low level vs High level.

Basic Implementation

// fireEvent (Low Level)
fireEvent.change(input, { target: { value: 'a' } });
// Just sets the value. Doesn't trigger focus, keydown, keyup.

// userEvent (High Level)
await userEvent.type(input, 'a');
// Triggers: focus -> keydown -> keypress -> input -> keyup

Real World Example

// If you have a form that validates on 'blur' or 'keydown':
// fireEvent.change() might NOT trigger the validation.
// userEvent.type() WILL trigger it, matching real user behavior.
Show Answer
  • fireEvent: Dispatches a DOM event directly. Fast but unrealistic (e.g., clicking a button doesn't focus it).
  • userEvent: Simulates full user interaction. Typing triggers keyDown, keyPress, input, keyUp. Always prefer userEvent.
Q: How to debug a failing test?
screen.debug()

Basic Implementation

test('debugging', () => {
  render(<App />);
  screen.debug(); // Prints HTML to console
  
  const btn = screen.getByRole('button');
  screen.debug(btn); // Prints only the button HTML
});

Real World Example

// Using Testing Playground
screen.logTestingPlaygroundURL();
// Generates a URL. Open it in browser to see your DOM 
// and get suggested queries (e.g., "screen.getByRole('heading')").
Show Answer

1. Use screen.debug() to print the current DOM to the console.
2. Use screen.logTestingPlaygroundURL() to get a link to an interactive sandbox.
3. Run the test in "watch mode" to see changes instantly.

Q: Testing Asynchronous Code?
waitFor / findBy.

Basic Implementation

// Option 1: findBy (Preferred for elements)
const item = await screen.findByText('Loaded Item');

// Option 2: waitFor (For assertions)
await waitFor(() => {
  expect(mockFn).toHaveBeenCalled();
});

Real World Example

// Waiting for an element to disappear
await waitFor(() => {
  expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
});
// OR
await waitForElementToBeRemoved(() => screen.queryByText('Loading...'));
Show Answer

Use await screen.findBy... when waiting for an element to appear. Use await waitFor(() => expect(...)) when waiting for an assertion to pass (e.g., a function to be called).

Q: What is "Flaky Test"?
Sometimes passes, sometimes fails.

Basic Implementation

// Flaky Test Example:
test('random fail', async () => {
  // Relies on network speed being fast
  await page.click('#submit');
  // Fails if server takes > 100ms
  expect(await page.innerText('#status')).toBe('Done'); 
});

Real World Example

// Fixing Flakiness:
// 1. Don't use fixed timeouts (wait(1000)).
// 2. Use assertions that retry (findBy, expect.toBeVisible).
// 3. Isolate state (reset DB between tests).
// 4. Mock external services (don't hit real APIs).
Show Answer

A test that produces different results with the same code. Causes: Network timing, animations, random data, shared state between tests. They destroy trust in the test suite.

Q: How to test timers (setTimeout)?
Fake Timers.

Basic Implementation

test('auto-logout after 5s', () => {
  vi.useFakeTimers();
  render(<App />);
  
  // Fast-forward time
  act(() => {
    vi.advanceTimersByTime(5000);
  });

  expect(screen.getByText('Logged out')).toBeInTheDocument();
  vi.useRealTimers();
});

Real World Example

// Debounce testing
test('search debounces input', () => {
  vi.useFakeTimers();
  const onSearch = vi.fn();
  render(<Search onSearch={onSearch} />);

  userEvent.type(input, 'react');
  
  // Should not call yet
  expect(onSearch).not.toHaveBeenCalled();
  
  // Fast forward debounce time
  act(() => vi.runAllTimers());
  
  expect(onSearch).toHaveBeenCalledWith('react');
});
Show Answer

Use Jest/Vitest Fake Timers.
vi.useFakeTimers();
vi.advanceTimersByTime(1000);
This allows you to fast-forward time instantly instead of waiting real seconds.

Q: Static Analysis vs Testing?
ESLint/TS vs Jest.

Basic Implementation

// Static Analysis (TypeScript)
const add = (a: number, b: number) => a + b;
add("1", 2); // Error: Argument of type 'string' is not assignable to 'number'.
// Catches this BEFORE running code.

// Testing (Jest)
test('add', () => {
  expect(add(1, 2)).toBe(3);
});
// Verifies logic is correct.

Real World Example

// Use both!
// TypeScript ensures you don't pass null to a function expecting a string.
// Tests ensure that function returns the correct string format.
// ESLint ensures you don't leave unused variables or console.logs.
Show Answer
  • Static Analysis (ESLint, TypeScript): Checks code without running it. Catches typos, type errors, syntax.
  • Testing: Runs the code to verify behavior.
  • Both are needed.
Q: What is "Mutation Testing"?
Testing your tests.

Basic Implementation

// Original Code
function isPositive(n) {
  return n > 0;
}

// Mutant 1 (Generated by Stryker)
return n >= 0; 

// If your test is: expect(isPositive(1)).toBe(true);
// It passes for BOTH original and mutant.
// The mutant "survived". You need a test case for 0.

Real World Example

// StrykerJS is a popular tool.
// It changes `+` to `-`, `true` to `false`, removes function calls.
// High mutation score = Robust tests.
// Low mutation score = Tests might be passing but not checking enough.
Show Answer

A technique where a tool (like Stryker) deliberately introduces bugs into your code (mutants) and runs your tests. If your tests still pass, the mutant "survived" (meaning your tests are weak). If a test fails, the mutant was "killed".

Q: How to test a custom hook that uses Context?
Wrapper.

Basic Implementation

const wrapper = ({ children }) => (
  <AuthProvider value={{ user: 'Alice' }}>
    {children}
  </AuthProvider>
);

const { result } = renderHook(() => useAuth(), { wrapper });
expect(result.current.user).toBe('Alice');

Real World Example

// Testing a Theme Hook
const { result } = renderHook(() => useTheme(), {
  wrapper: ({ children }) => <ThemeProvider theme="dark">{children}</ThemeProvider>
});

expect(result.current.mode).toBe('dark');
Show Answer
const wrapper = ({ children }) => (
  <AuthProvider>{children}</AuthProvider>
);
const { result } = renderHook(() => useAuth(), { wrapper });
Q: What is "Fuzz Testing"?
Random inputs.

Basic Implementation

// Property based testing is a form of fuzzing.
// Generating random strings, huge numbers, emojis, nulls.

test('parser handles garbage', () => {
  const randomInput = generateRandomString(1000); // "åß∂ƒ©˙∆..."
  expect(() => parse(randomInput)).not.toThrow();
});

Real World Example

// Security Fuzzing:
// Sending malformed JSON or SQL injection strings to an API endpoint
// to see if it crashes or leaks data.
Show Answer

Automated software testing technique that involves providing invalid, unexpected, or random data as inputs to a computer program to find crashes or leaks.

Q: Should you test private methods?
Black box.

Basic Implementation

// Class
class Calculator {
  public add(a, b) { return this.validate(a) + this.validate(b); }
  private validate(n) { return Number(n); }
}

// Test
// ✅ Test public API
expect(calc.add(1, 2)).toBe(3);

// ❌ Don't test private
// expect(calc.validate(1)).toBe(1);

Real World Example

// If you test private methods, you lock the implementation.
// If you later decide to rename `validate` to `parseNumber`,
// your test fails even though the public behavior (add) is still correct.
// This increases maintenance burden.
Show Answer

Generally No. Test the public interface (props, user interactions). Private methods are implementation details. If you test them, refactoring becomes hard because you have to update tests even if behavior didn't change.

Q: How to test responsive design?
Viewport size.

Basic Implementation

// Playwright
test('mobile layout', async ({ page }) => {
  await page.setViewportSize({ width: 375, height: 667 });
  await expect(page.locator('.hamburger-menu')).toBeVisible();
});

test('desktop layout', async ({ page }) => {
  await page.setViewportSize({ width: 1024, height: 768 });
  await expect(page.locator('.hamburger-menu')).toBeHidden();
});

Real World Example

// In Unit Tests (Jest/Vitest)
// You can mock window.matchMedia
window.matchMedia = vi.fn().mockImplementation(query => ({
  matches: query === '(max-width: 600px)',
  // ...
}));
Show Answer

In Cypress/Playwright, you can set the viewport size: cy.viewport('iphone-6'). In Unit tests, it's harder, but you can mock window.matchMedia.

Q: What is "Contract Testing"?
API agreements.

Basic Implementation

// Consumer (Frontend) defines expectation:
// "I expect GET /user/1 to return { id: 1, name: string }"

// Provider (Backend) verifies:
// "Does my endpoint actually return that?"

Real World Example

// Using Pact
// 1. Frontend runs test, generates a "Pact file" (JSON contract).
// 2. Pact file is uploaded to a broker.
// 3. Backend CI downloads the Pact file and replays requests against itself.
// 4. If Backend changed the API (e.g., renamed 'name' to 'fullName'), the build fails.
Show Answer

Ensures that the frontend and backend agree on the API structure (Contract). Tools like Pact verify that the provider (API) sends what the consumer (Frontend) expects.

Q: Mocking vs. Stubbing vs. Spying?
Test doubles.

Basic Implementation

// Stub: Replaces behavior completely
vi.fn().mockReturnValue(42);

// Spy: Listens to calls, can pass through to original
const spy = vi.spyOn(console, 'log');
console.log('hi');
expect(spy).toHaveBeenCalledWith('hi');

// Mock: Object with expectations
const mockUser = {
  getName: vi.fn().mockReturnValue('Bob')
};

Real World Example

// Spying is useful when you want to verify a side effect
// without stopping it (or while stopping it).
// Example: Spy on window.open to ensure a link opens a new tab,
// but prevent the test runner from actually opening a window.
vi.spyOn(window, 'open').mockImplementation(() => {});
Show Answer
  • Stub: Replaces a function with a fixed return value.
  • Spy: Wraps a function to record arguments and calls, but (optionally) executes the real code.
  • Mock: A fake object with pre-programmed expectations.
Q: How to test LocalStorage?
Mocking window.

Basic Implementation

test('saves to local storage', async () => {
  const setItemSpy = vi.spyOn(Storage.prototype, 'setItem');
  
  render(<ThemeSwitcher />);
  await userEvent.click(screen.getByText('Dark Mode'));
  
  expect(setItemSpy).toHaveBeenCalledWith('theme', 'dark');
});

Real World Example

// JSDOM implements localStorage, so you can also check values directly:
expect(localStorage.getItem('theme')).toBe('dark');

// Remember to clear it between tests!
afterEach(() => {
  localStorage.clear();
});
Show Answer

Since tests run in JSDOM (which supports localStorage), you can just use it. Or, mock the Storage prototype to assert calls.

Q: What is "Happy Path" vs "Unhappy Path"?
Success vs Error.

Basic Implementation

// Happy Path
test('login success', () => {
  mockLogin.mockResolvedValue({ token: '123' });
  // ... assert redirect to dashboard
});

// Unhappy Path
test('login failure', () => {
  mockLogin.mockRejectedValue(new Error('Invalid credentials'));
  // ... assert error message is shown
});

Real World Example

// Developers often forget Unhappy Paths.
// - Network timeout?
// - Empty search results?
// - 500 Server Error?
// - Invalid file format upload?
// Testing these ensures your app fails gracefully.
Show Answer
  • Happy Path: The default scenario where everything works (User logs in successfully).
  • Unhappy Path: Edge cases and errors (Wrong password, Server down, Network timeout). Crucial to test.
Q: Testing Accessibility (A11y)?
jest-axe.

Basic Implementation

import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);

test('should have no accessibility violations', async () => {
  const { container } = render(<App />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

Real World Example

// Catches common errors:
// - Low color contrast
// - Missing alt text on images
// - Missing labels on inputs
// - Duplicate IDs
// Does NOT catch everything (e.g., logical tab order), manual testing still needed.
Show Answer

Use jest-axe to run automated accessibility checks on your rendered DOM.

const { container } = render(<App />);
const results = await axe(container);
expect(results).toHaveNoViolations();
Q: CI/CD Integration?
Automating tests.

Basic Implementation

# .github/workflows/test.yml
name: Run Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - run: npm install
      - run: npm test
      - run: npm run e2e

Real World Example

// Block merging if tests fail.
// Configure GitHub "Branch Protection Rules":
// "Require status checks to pass before merging" -> Select "test" job.
Show Answer

Tests should run automatically on every Pull Request (via GitHub Actions, Jenkins). If tests fail, the merge should be blocked.

Q: Snapshot Testing Best Practices?
Small and focused.

Basic Implementation

// ❌ Bad: Snapshotting entire App
render(<App />);
expect(container).toMatchSnapshot(); 
// Result: 5000 line snapshot file. Impossible to review.

// ✅ Good: Snapshotting a specific prop variation
render(<Card variant="featured" />);
expect(container).toMatchSnapshot();
// Result: 20 line snapshot. Easy to see if class changed.

Real World Example

// Use "Inline Snapshots" for very small outputs.
expect(generateSlug('Hello World')).toMatchInlineSnapshot(`"hello-world"`);
// The snapshot is written directly into your test file, making it visible.
Show Answer

Keep snapshots small. Don't snapshot the whole <App />. Snapshot small, presentational components. Review snapshot diffs carefully in PRs.

Q: How to test Drag and Drop?
Complex events.

Basic Implementation

// Playwright (E2E) - Recommended
await page.dragAndDrop('#source', '#target');

// React Testing Library (Unit) - Harder
fireEvent.dragStart(source);
fireEvent.dragEnter(target);
fireEvent.dragOver(target);
fireEvent.drop(target);
// Often requires mocking dataTransfer object.

Real World Example

// If using a library like `dnd-kit` or `react-beautiful-dnd`,
// they often expose specific testing utilities or instructions.
// E2E is usually more reliable for this interaction.
Show Answer

Difficult in JSDOM. Libraries like dnd-kit provide testing utilities. Often easier to test in E2E (Playwright) where real mouse events exist.

5. Advanced Testing Patterns

Q: What is "Property Based Testing"?
fast-check.

Basic Implementation

import fc from 'fast-check';

test('addition is commutative', () => {
  fc.assert(
    fc.property(fc.integer(), fc.integer(), (a, b) => {
      return a + b === b + a;
    })
  );
});

Real World Example

// Testing a sort function:
// Property: "The output array should have the same length as input"
// Property: "The output array should be sorted"
// fast-check will generate empty arrays, arrays with 1 item, arrays with duplicates, etc.
Show Answer

Instead of testing specific inputs (1 + 1 = 2), you define properties that should always hold true (e.g., for any numbers a and b, a + b = b + a). The tool generates thousands of random inputs to try and break your code.

Q: How to test WebSockets?
Mocking the server.

Basic Implementation

import WS from 'jest-websocket-mock';

test('receives message', async () => {
  const server = new WS('ws://localhost:1234');
  const client = new WebSocket('ws://localhost:1234');
  
  await server.connected;
  
  client.onmessage = (e) => {
    expect(e.data).toBe('hello');
  };
  
  server.send('hello');
  server.close();
});

Real World Example

// Testing a Chat App
// 1. Connect client.
// 2. Mock server sending a "New Message" payload.
// 3. Assert that the message appears in the UI list.
Show Answer

You can mock the WebSocket object in the browser environment. Libraries like mock-socket allow you to simulate a server sending messages to your client and verify how the client reacts.

Q: What is "Chaos Engineering" in frontend?
Gremlins.js.

Basic Implementation

// Gremlins.js
import gremlins from 'gremlins.js';

gremlins.createHorde().unleash();
// Randomly clicks, scrolls, touches, types everywhere.

Real World Example

// Run this on a staging environment.
// Check console for errors.
// Did the app crash? Did it freeze?
// Helps find memory leaks or unhandled exceptions in obscure UI states.
Show Answer

Intentionally injecting failures (network latency, 500 errors, random clicks) into the frontend to see if it recovers gracefully. Tools like Gremlins.js unleash a horde of "gremlins" to click everywhere randomly.

Q: How to test Canvas / WebGL?
Visuals.

Basic Implementation

// Functional testing is hard because Canvas is a black box to DOM.
// Strategy: Visual Regression.

test('canvas draws circle', async ({ page }) => {
  await page.goto('/canvas-demo');
  await expect(page.locator('canvas')).toHaveScreenshot('circle.png');
});

Real World Example

// Alternatively, test the logic that *drives* the canvas.
// If you have a `drawCircle(x, y, radius)` function, test that function's math
// separately from the actual rendering context.
Show Answer

Since Canvas is just pixels, you can't query elements like DOM. You rely heavily on Visual Regression Testing (snapshots of the canvas) or testing the internal state logic that drives the canvas rendering.

Q: What is the "Testing Trophy"?
Kent C. Dodds.

Basic Implementation

// The Trophy Shape:
// 🏆
// E2E (Top, Smallest)
// Integration (Middle, Largest)
// Unit (Bottom, Medium)
// Static (Base, Broad)

Real World Example

// Contrast with "Testing Pyramid" (which emphasizes Unit tests).
// Modern React apps rely heavily on composition and hooks.
// Integration tests (testing components together) give the best "Confidence per Dollar".
Show Answer

A metaphor prioritizing Integration Tests.
Top: E2E (Few)
Middle: Integration (Many) - "Write tests. Not too many. Mostly integration."
Bottom: Unit (Some)
Base: Static Analysis (Always)

Q: How to test Service Workers (PWA)?
Browser context.

Basic Implementation

// Playwright
test('works offline', async ({ page }) => {
  await page.goto('/');
  await page.context().setOffline(true);
  
  await page.reload();
  await expect(page.locator('h1')).toHaveText('My App');
});

Real World Example

// Verify that the Service Worker cached the assets.
// Verify that requests are served from Service Worker (check Network tab via protocol).
Show Answer

Best tested in E2E (Playwright/Cypress) because they rely on browser APIs not present in JSDOM. You can verify offline behavior by toggling network status in the test.

Q: What is "Shift Left" testing?
Earlier.

Basic Implementation

// Traditional (Waterfall):
// Design -> Code -> Test -> Deploy
// Bug found in Test phase = Expensive fix.

// Shift Left:
// Design -> Test/Code -> Deploy
// Bug found in Code phase (via Unit/Lint) = Cheap fix.

Real World Example

// Implementing Pre-commit hooks (Husky).
// Running tests on every file save.
// Writing tests *before* code (TDD).
// All these move testing "left" on the timeline.
Show Answer

Moving testing earlier in the development cycle. Instead of testing just before release, you test during development (Unit tests, PR checks). It makes fixing bugs cheaper and faster.

Q: How to test authentication flows (OAuth)?
Programmatic login.

Basic Implementation

// Playwright: Reuse state
test.use({ storageState: 'auth.json' });

test('dashboard access', async ({ page }) => {
  await page.goto('/dashboard');
  // Already logged in!
});

Real World Example

// Global Setup:
// 1. Run a setup script that logs in via API (POST /login).
// 2. Save the returned Cookies/LocalStorage to 'auth.json'.
// 3. All tests reuse this file.
// Saves massive amounts of time vs logging in via UI for every test.
Show Answer

In E2E tests, don't type username/password in the UI every time (slow). Instead, make a direct API request to get a session token, set it in the browser's LocalStorage/Cookie, and reload. This is "Programmatic Login".

Q: What is "A/B Testing" from a dev perspective?
Feature Flags.

Basic Implementation

// Code
const showNewFeature = useFeatureFlag('new-checkout');
return showNewFeature ? <NewCheckout /> : <OldCheckout />;

// Test
test('renders new checkout when flag is on', () => {
  mockFlags({ 'new-checkout': true });
  render(<App />);
  expect(screen.getByText('New Checkout')).toBeInTheDocument();
});

Real World Example

// Tools like LaunchDarkly or Split.io.
// Tests must ensure that BOTH paths work.
// Don't delete the old code/tests until the A/B test is 100% complete and the old feature is retired.
Show Answer

Serving different versions of a feature to different users. Developers implement this using Feature Flags. Tests should verify both paths (Flag ON and Flag OFF).

Q: How to test file uploads?
userEvent.upload.

Basic Implementation

test('upload file', async () => {
  const file = new File(['(content)'], 'hello.png', { type: 'image/png' });
  const input = screen.getByLabelText(/upload/i);
  
  await userEvent.upload(input, file);
  
  expect(input.files[0]).toBe(file);
  expect(input.files.item(0)).toBe(file);
});

Real World Example

// Testing file size validation
const hugeFile = new File([''], 'huge.png');
Object.defineProperty(hugeFile, 'size', { value: 1024 * 1024 * 10 }); // 10MB

await userEvent.upload(input, hugeFile);
expect(screen.getByText('File too large')).toBeVisible();
Show Answer
const file = new File(['hello'], 'hello.png', { type: 'image/png' });
const input = screen.getByLabelText(/upload/i);
await userEvent.upload(input, file);
expect(input.files[0]).toBe(file);
Q: What is "Smoke Testing"?
Is it on fire?

Basic Implementation

// A simple E2E test that runs after deployment
test('smoke test', async ({ page }) => {
  await page.goto('https://production.com');
  await expect(page).toHaveTitle(/My App/);
  await expect(page.locator('#root')).toBeVisible();
});

Real World Example

// If this fails, rollback immediately.
// It doesn't check deep functionality, just "Did the server start?" and "Is the page white?".
Show Answer

A quick set of tests run after a build to ensure the critical functions work (e.g., "Can the app load?", "Can I log in?"). If smoke tests fail, there's no point running the full suite.

Q: How to test infinite scroll?
Intersection Observer.

Basic Implementation

// Mock IntersectionObserver
beforeEach(() => {
  window.IntersectionObserver = vi.fn((callback) => ({
    observe: vi.fn(),
    disconnect: vi.fn(),
    // Manually trigger callback to simulate scroll
    trigger: (entries) => callback(entries)
  }));
});

Real World Example

// In test:
render(<InfiniteList />);
// Simulate element coming into view
act(() => {
  const observer = window.IntersectionObserver.mock.results[0].value;
  observer.trigger([{ isIntersecting: true }]);
});
expect(fetchMoreData).toHaveBeenCalled();
Show Answer

You need to mock the IntersectionObserver API in JSDOM. In the test, you manually trigger the observer callback to simulate the user scrolling to the bottom.

Q: What is "Regression Testing"?
Did we break it?

Basic Implementation

// Scenario:
// 1. Bug reported: "Login fails if password has space".
// 2. Fix bug.
// 3. Write test case: "Login with space in password".
// 4. This test is now part of the "Regression Suite".

Real World Example

// Every time you run your full test suite, you are doing Regression Testing.
// You are verifying that new changes didn't break old features.
Show Answer

Re-running functional and non-functional tests to ensure that previously developed and tested software still performs after a change. Automated suites are essentially regression suites.

Q: How to test iFrames?
Context switching.

Basic Implementation

// Playwright
const frame = page.frameLocator('#payment-iframe');
await frame.getByPlaceholder('Card Number').fill('4242 4242...');

Real World Example

// Common in Payment Gateways (Stripe elements) or embedded widgets.
// You cannot access iframe content from the main page context due to security (CORS)
// unless you explicitly switch context in the test runner.
Show Answer

In Playwright, you have to switch context to the frame: page.frameLocator('#my-frame').getByText('Submit'). Standard getBy selectors won't find elements inside an iframe.

Q: What is "Performance Testing" in frontend?
Lighthouse CI.

Basic Implementation

// Lighthouse CI (lighthouserc.js)
module.exports = {
  ci: {
    assert: {
      assertions: {
        'categories:performance': ['error', { minScore: 0.9 }],
        'first-contentful-paint': ['warn', { maxNumericValue: 2000 }],
      },
    },
  },
};

Real World Example

// Run in CI pipeline.
// If a developer adds a 5MB image, the performance score drops.
// The build fails, preventing the regression from reaching production.
Show Answer

Measuring metrics like LCP (Largest Contentful Paint) and TBT (Total Blocking Time). You can run Lighthouse in your CI pipeline to fail the build if performance drops below a threshold.

Q: How to test Error Boundaries?
Throwing errors.

Basic Implementation

const Bomb = () => { throw new Error('Boom'); };

test('catches error', () => {
  // Prevent console.error from cluttering output
  const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});

  render(
    <ErrorBoundary fallback={<div>Error Caught</div>}>
      <Bomb />
    </ErrorBoundary>
  );

  expect(screen.getByText('Error Caught')).toBeInTheDocument();
  consoleSpy.mockRestore();
});

Real World Example

// Ensure your app doesn't white-screen on crash.
// Test that the "Try Again" button in the Error Boundary actually resets the state.
Show Answer

Create a component that throws an error (e.g., const Bomb = () => { throw new Error('Boom') }). Render it inside your Error Boundary. Assert that the fallback UI is displayed and the error was logged (you might need to suppress console.error in the test).