15+ Testing Strategies Interview Questions 2025: Unit, Integration & E2E Testing

·21 min read
testingunit-testingintegration-testinge2e-testingjavascriptinterview-preparation

Testing questions reveal how you think about code quality and maintainability. Interviewers aren't looking for someone who can recite testing frameworks—they want developers who understand what to test, when to test it, and the trade-offs between different approaches.

Table of Contents

  1. Testing Fundamentals Questions
  2. Unit Testing Questions
  3. Integration Testing Questions
  4. E2E Testing Questions
  5. React/Frontend Testing Questions
  6. API/Backend Testing Questions
  7. Testing Best Practices Questions
  8. Interview Challenge Questions
  9. Quick Reference

Testing Fundamentals Questions

These questions test your understanding of testing philosophies and when to use each approach.

What is the difference between the testing pyramid and testing trophy?

The testing pyramid and testing trophy are two competing philosophies for how to distribute your test coverage across different test types. Understanding both helps you make informed decisions about your testing strategy.

Testing Pyramid (traditional):

flowchart TB
    subgraph PYRAMID["Testing Pyramid"]
        E2E["E2E Tests<br/><i>Few, slow, high confidence</i>"]
        INT["Integration Tests<br/><i>Some, medium speed</i>"]
        UNIT["Unit Tests<br/><i>Many, fast, low confidence per test</i>"]
    end
 
    UNIT --> INT --> E2E
 
    style E2E fill:#dc2626,color:#ffffff
    style INT fill:#f59e0b,color:#000000
    style UNIT fill:#22c55e,color:#ffffff

Testing Trophy (Kent C. Dodds):

flowchart TB
    subgraph TROPHY["Testing Trophy"]
        E2E["E2E Tests<br/><i>Few</i>"]
        INT["Integration Tests<br/><i>Most tests here</i>"]
        UNIT["Unit Tests<br/><i>Some</i>"]
        STATIC["Static Analysis<br/><i>TypeScript, ESLint</i>"]
    end
 
    STATIC --> UNIT --> INT --> E2E
 
    style E2E fill:#dc2626,color:#ffffff
    style INT fill:#6366f1,color:#ffffff
    style UNIT fill:#f59e0b,color:#000000
    style STATIC fill:#64748b,color:#ffffff

The trophy argues that integration tests give the best confidence-to-effort ratio. Unit tests are fast but test in isolation—bugs often live in the gaps between units.

Your AppRecommendation
Heavy business logicMore unit tests (pyramid)
UI-heavy, user flowsMore integration tests (trophy)
Complex algorithmsUnit tests for logic
CRUD applicationIntegration tests for APIs

What makes a good test?

Good tests follow the FIRST principles: Fast, Isolated, Repeatable, Self-validating, and Timely. These principles ensure your tests provide reliable feedback without becoming a maintenance burden.

FIRST principles:

  • Fast: Milliseconds, not seconds
  • Isolated: No shared state between tests
  • Repeatable: Same result every time
  • Self-validating: Pass or fail, no manual checking
  • Timely: Written close to the code

What to test:

  • Business logic and calculations
  • Edge cases and error handling
  • User-facing functionality
  • Integration points (APIs, databases)

What NOT to test:

  • Third-party library internals
  • Language features
  • Trivial getters/setters
  • Implementation details (test behavior, not how)

How do testing levels trade off speed vs confidence?

Every testing level involves a fundamental trade-off between how fast the tests run and how much confidence they provide. Understanding this trade-off helps you allocate testing effort appropriately.

quadrantChart
    title Speed vs Confidence Trade-off
    x-axis Fast --> Slow
    y-axis Low Confidence --> High Confidence
    quadrant-1 Slow but High Confidence
    quadrant-2 Fast and High Confidence
    quadrant-3 Fast but Low Confidence
    quadrant-4 Slow and Low Confidence
    Unit Tests: [0.2, 0.3]
    Integration Tests: [0.5, 0.6]
    E2E Tests: [0.8, 0.9]
LevelSpeedConfidenceMaintenance
UnitFastLowerLow
IntegrationMediumHigherMedium
E2ESlowHighestHigh

Unit Testing Questions

These questions test your ability to write isolated, focused tests.

How do you write effective unit tests?

Effective unit tests focus on testing one thing in isolation, with clear inputs and expected outputs. They should be easy to understand, fast to run, and provide clear failure messages when something breaks.

// Function to test
function calculateDiscount(price, discountPercent) {
  if (discountPercent < 0 || discountPercent > 100) {
    throw new Error('Invalid discount percentage');
  }
  return price * (1 - discountPercent / 100);
}
 
// Good unit tests
describe('calculateDiscount', () => {
  it('applies percentage discount correctly', () => {
    expect(calculateDiscount(100, 20)).toBe(80);
  });
 
  it('handles zero discount', () => {
    expect(calculateDiscount(100, 0)).toBe(100);
  });
 
  it('handles 100% discount', () => {
    expect(calculateDiscount(100, 100)).toBe(0);
  });
 
  it('throws for negative discount', () => {
    expect(() => calculateDiscount(100, -10)).toThrow('Invalid discount');
  });
 
  it('throws for discount over 100', () => {
    expect(() => calculateDiscount(100, 150)).toThrow('Invalid discount');
  });
});

What is the Arrange-Act-Assert pattern?

The Arrange-Act-Assert (AAA) pattern provides a consistent structure for organizing test code. It separates setup, execution, and verification into distinct phases, making tests easier to read and maintain.

it('sends welcome email to new user', async () => {
  // Arrange - set up test data and mocks
  const user = { email: 'test@example.com', name: 'Alice' };
  const mockEmailService = { send: jest.fn().mockResolvedValue(true) };
 
  // Act - execute the code under test
  await sendWelcomeEmail(user, mockEmailService);
 
  // Assert - verify the results
  expect(mockEmailService.send).toHaveBeenCalledWith({
    to: 'test@example.com',
    subject: 'Welcome, Alice!',
    template: 'welcome'
  });
});

What is the difference between mocking, stubbing, and spying?

Mocks, stubs, and spies are test doubles that replace real dependencies during testing. Each serves a different purpose and understanding when to use each helps you write more effective tests.

Mock: Replace a function/module entirely

// Mock entire module
jest.mock('./emailService');
 
import { sendEmail } from './emailService';
 
sendEmail.mockResolvedValue({ success: true });

Stub: Provide canned responses

// Stub specific behavior
const stub = jest.fn()
  .mockReturnValueOnce('first call')
  .mockReturnValueOnce('second call')
  .mockReturnValue('default');

Spy: Watch a real function without replacing it

// Spy on existing method
const spy = jest.spyOn(console, 'log');
 
doSomething();
 
expect(spy).toHaveBeenCalledWith('Expected message');
spy.mockRestore(); // Clean up
MockDon't Mock
External APIsThe code you're testing
Database callsPure functions
Time/datesSimple dependencies
File systemIn-memory alternatives

How do you test async code?

Testing asynchronous code requires special handling to ensure tests wait for async operations to complete. Jest provides several patterns for handling promises, timers, and error cases.

// Promises
it('fetches user data', async () => {
  const user = await fetchUser(1);
  expect(user.name).toBe('Alice');
});
 
// With Jest fake timers
it('retries after delay', async () => {
  jest.useFakeTimers();
 
  const promise = retryWithDelay(mockFn, 3, 1000);
 
  // Fast-forward time
  jest.advanceTimersByTime(3000);
 
  await promise;
  expect(mockFn).toHaveBeenCalledTimes(3);
 
  jest.useRealTimers();
});
 
// Testing error handling
it('throws on network error', async () => {
  mockFetch.mockRejectedValue(new Error('Network error'));
 
  await expect(fetchUser(1)).rejects.toThrow('Network error');
});

Integration Testing Questions

These questions test your understanding of testing multiple components together.

When do integration tests catch bugs that unit tests miss?

Integration tests excel at finding bugs that exist in the boundaries between components. A unit test might verify each function works in isolation, but integration tests ensure they work correctly when combined.

// Unit test passes - each function works alone
// But integration might fail if they don't work together
 
// Integration test for user registration flow
describe('User Registration', () => {
  let app;
  let db;
 
  beforeAll(async () => {
    db = await setupTestDatabase();
    app = createApp(db);
  });
 
  afterAll(async () => {
    await db.close();
  });
 
  beforeEach(async () => {
    await db.clear(); // Clean state
  });
 
  it('registers user and sends verification email', async () => {
    const response = await request(app)
      .post('/api/register')
      .send({
        email: 'test@example.com',
        password: 'securepass123'
      });
 
    expect(response.status).toBe(201);
 
    // Verify user in database
    const user = await db.users.findByEmail('test@example.com');
    expect(user).toBeDefined();
    expect(user.verified).toBe(false);
 
    // Verify email was queued
    const emails = await db.emailQueue.findAll();
    expect(emails).toHaveLength(1);
    expect(emails[0].template).toBe('verify-email');
  });
});

How do you test with real databases?

Testing with real databases provides higher confidence than mocks but requires careful setup and teardown. There are three main approaches, each with different trade-offs.

Option 1: Test database

// Use separate test database
const config = {
  test: {
    database: 'myapp_test',
    // ... other config
  }
};
 
beforeEach(async () => {
  await db.migrate.latest();
  await db.seed.run();
});
 
afterEach(async () => {
  await db.migrate.rollback();
});

Option 2: Test containers

// Using testcontainers
import { PostgreSqlContainer } from '@testcontainers/postgresql';
 
let container;
let db;
 
beforeAll(async () => {
  container = await new PostgreSqlContainer().start();
  db = await createConnection(container.getConnectionUri());
}, 60000); // Longer timeout for container startup
 
afterAll(async () => {
  await db.close();
  await container.stop();
});

Option 3: In-memory database

// SQLite in-memory for fast tests
const db = new Database(':memory:');
 
// Or with Prisma
datasource db {
  provider = "sqlite"
  url      = "file::memory:"
}

How do you write API integration tests?

API integration tests verify that your endpoints correctly handle requests, interact with the database, and return appropriate responses. Supertest is the standard tool for testing Express APIs.

// Testing Express API with Supertest
import request from 'supertest';
import app from '../app';
 
describe('POST /api/orders', () => {
  it('creates order with valid data', async () => {
    const orderData = {
      items: [{ productId: 1, quantity: 2 }],
      shippingAddress: { city: 'NYC', zip: '10001' }
    };
 
    const response = await request(app)
      .post('/api/orders')
      .set('Authorization', `Bearer ${testUserToken}`)
      .send(orderData)
      .expect(201);
 
    expect(response.body).toMatchObject({
      id: expect.any(Number),
      status: 'pending',
      items: expect.arrayContaining([
        expect.objectContaining({ productId: 1 })
      ])
    });
  });
 
  it('returns 401 without authentication', async () => {
    await request(app)
      .post('/api/orders')
      .send({})
      .expect(401);
  });
 
  it('returns 400 with invalid data', async () => {
    const response = await request(app)
      .post('/api/orders')
      .set('Authorization', `Bearer ${testUserToken}`)
      .send({ items: [] })
      .expect(400);
 
    expect(response.body.error).toContain('items');
  });
});

E2E Testing Questions

These questions test your knowledge of browser automation and end-to-end testing.

What are the main browser automation tools?

Cypress and Playwright are the two leading E2E testing frameworks. Cypress offers an excellent developer experience with its test runner, while Playwright provides better cross-browser support and parallelization.

Cypress:

// cypress/e2e/checkout.cy.js
describe('Checkout Flow', () => {
  beforeEach(() => {
    cy.visit('/');
    cy.login('test@example.com', 'password');
  });
 
  it('completes purchase successfully', () => {
    // Add item to cart
    cy.get('[data-testid="product-card"]').first().click();
    cy.get('[data-testid="add-to-cart"]').click();
 
    // Go to checkout
    cy.get('[data-testid="cart-icon"]').click();
    cy.get('[data-testid="checkout-button"]').click();
 
    // Fill shipping
    cy.get('#address').type('123 Main St');
    cy.get('#city').type('New York');
    cy.get('#zip').type('10001');
 
    // Complete order
    cy.get('[data-testid="place-order"]').click();
 
    // Verify success
    cy.url().should('include', '/order-confirmation');
    cy.contains('Thank you for your order');
  });
});

Playwright:

// tests/checkout.spec.js
import { test, expect } from '@playwright/test';
 
test.describe('Checkout Flow', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/');
    await page.fill('#email', 'test@example.com');
    await page.fill('#password', 'password');
    await page.click('button[type="submit"]');
  });
 
  test('completes purchase successfully', async ({ page }) => {
    // Add item to cart
    await page.click('[data-testid="product-card"] >> nth=0');
    await page.click('[data-testid="add-to-cart"]');
 
    // Go to checkout
    await page.click('[data-testid="cart-icon"]');
    await page.click('[data-testid="checkout-button"]');
 
    // Fill shipping
    await page.fill('#address', '123 Main St');
    await page.fill('#city', 'New York');
    await page.fill('#zip', '10001');
 
    // Complete order
    await page.click('[data-testid="place-order"]');
 
    // Verify success
    await expect(page).toHaveURL(/order-confirmation/);
    await expect(page.locator('text=Thank you')).toBeVisible();
  });
});

How do you fix flaky E2E tests?

Flaky tests are tests that sometimes pass and sometimes fail without code changes. They're often caused by timing issues, shared state, or unstable selectors. Here's how to fix the most common causes.

// BAD: Arbitrary wait
cy.wait(3000);
await page.waitForTimeout(3000);
 
// GOOD: Wait for specific condition
cy.get('[data-testid="results"]').should('be.visible');
await page.waitForSelector('[data-testid="results"]');
 
// GOOD: Wait for network idle
await page.waitForLoadState('networkidle');
 
// GOOD: Retry assertions (Playwright has this built-in)
await expect(page.locator('.count')).toHaveText('5', { timeout: 10000 });

Test isolation:

// Each test gets clean state
beforeEach(async () => {
  // Reset database
  await resetTestData();
 
  // Clear cookies/storage
  await context.clearCookies();
 
  // Or use fresh browser context per test
});

Stable selectors:

// BAD: Brittle selectors
cy.get('.btn-primary');
cy.get('div > span:nth-child(2)');
 
// GOOD: Test IDs
cy.get('[data-testid="submit-button"]');
 
// GOOD: Accessible selectors
cy.findByRole('button', { name: 'Submit' });
cy.findByLabelText('Email address');

How do you run E2E tests in CI/CD?

Running E2E tests in CI/CD requires starting your application, waiting for it to be ready, and properly handling artifacts like screenshots on failure.

# GitHub Actions
name: E2E Tests
 
on: [push, pull_request]
 
jobs:
  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: '20'
 
      - name: Install dependencies
        run: npm ci
 
      - name: Start application
        run: npm run start:test &
 
      - name: Wait for app
        run: npx wait-on http://localhost:3000
 
      - name: Run Cypress
        uses: cypress-io/github-action@v6
        with:
          wait-on: 'http://localhost:3000'
 
      - name: Upload screenshots on failure
        uses: actions/upload-artifact@v3
        if: failure()
        with:
          name: cypress-screenshots
          path: cypress/screenshots

React/Frontend Testing Questions

These questions test your knowledge of testing React components and hooks.

How do you test React components with React Testing Library?

React Testing Library encourages testing components the way users interact with them—by querying for elements using accessible names and roles rather than implementation details.

// UserProfile.test.jsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserProfile } from './UserProfile';
 
describe('UserProfile', () => {
  it('displays user information', () => {
    render(<UserProfile user={{ name: 'Alice', email: 'alice@example.com' }} />);
 
    expect(screen.getByText('Alice')).toBeInTheDocument();
    expect(screen.getByText('alice@example.com')).toBeInTheDocument();
  });
 
  it('allows editing profile', async () => {
    const onSave = jest.fn();
    const user = userEvent.setup();
 
    render(
      <UserProfile
        user={{ name: 'Alice', email: 'alice@example.com' }}
        onSave={onSave}
      />
    );
 
    // Click edit button
    await user.click(screen.getByRole('button', { name: /edit/i }));
 
    // Change name
    const nameInput = screen.getByLabelText(/name/i);
    await user.clear(nameInput);
    await user.type(nameInput, 'Alicia');
 
    // Save
    await user.click(screen.getByRole('button', { name: /save/i }));
 
    expect(onSave).toHaveBeenCalledWith({
      name: 'Alicia',
      email: 'alice@example.com'
    });
  });
});

How do you test custom React hooks?

Testing custom hooks requires the renderHook utility from React Testing Library. State updates must be wrapped in act() to properly simulate React's update cycle.

// useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
 
describe('useCounter', () => {
  it('initializes with default value', () => {
    const { result } = renderHook(() => useCounter());
    expect(result.current.count).toBe(0);
  });
 
  it('initializes with custom value', () => {
    const { result } = renderHook(() => useCounter(10));
    expect(result.current.count).toBe(10);
  });
 
  it('increments counter', () => {
    const { result } = renderHook(() => useCounter());
 
    act(() => {
      result.current.increment();
    });
 
    expect(result.current.count).toBe(1);
  });
 
  it('decrements counter', () => {
    const { result } = renderHook(() => useCounter(5));
 
    act(() => {
      result.current.decrement();
    });
 
    expect(result.current.count).toBe(4);
  });
});

How do you mock API calls in React tests?

MSW (Mock Service Worker) is the recommended approach for mocking API calls in tests. It intercepts network requests at the network level, making your tests more realistic than mocking fetch directly.

// Using MSW (Mock Service Worker)
import { rest } from 'msw';
import { setupServer } from 'msw/node';
 
const server = setupServer(
  rest.get('/api/user', (req, res, ctx) => {
    return res(ctx.json({ name: 'Alice', email: 'alice@example.com' }));
  })
);
 
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
 
it('displays user from API', async () => {
  render(<UserProfile userId={1} />);
 
  // Loading state
  expect(screen.getByText(/loading/i)).toBeInTheDocument();
 
  // Wait for data
  await waitFor(() => {
    expect(screen.getByText('Alice')).toBeInTheDocument();
  });
});
 
it('handles API error', async () => {
  // Override handler for this test
  server.use(
    rest.get('/api/user', (req, res, ctx) => {
      return res(ctx.status(500));
    })
  );
 
  render(<UserProfile userId={1} />);
 
  await waitFor(() => {
    expect(screen.getByText(/error/i)).toBeInTheDocument();
  });
});

When should you use snapshot testing?

Snapshot testing captures the rendered output of a component and compares it against a stored snapshot. It's useful for detecting unintended changes but can become noisy if overused.

// Good use: stable UI components
it('renders button variants correctly', () => {
  const { container } = render(
    <>
      <Button variant="primary">Primary</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="danger">Danger</Button>
    </>
  );
 
  expect(container).toMatchSnapshot();
});
 
// Bad use: frequently changing components
// Snapshot tests become noise when they change often

Snapshot best practices:

  • Use for stable, presentational components
  • Keep snapshots small and focused
  • Review snapshot changes carefully
  • Don't snapshot dynamic content (dates, IDs)

API/Backend Testing Questions

These questions test your knowledge of testing backend services and APIs.

How do you test Express middleware?

Testing middleware requires creating mock request and response objects. The middleware function is called with these mocks, and you verify the correct behavior based on different scenarios.

// authMiddleware.test.js
import { authMiddleware } from './authMiddleware';
import { verifyToken } from './jwt';
 
jest.mock('./jwt');
 
describe('authMiddleware', () => {
  let req, res, next;
 
  beforeEach(() => {
    req = { headers: {} };
    res = {
      status: jest.fn().mockReturnThis(),
      json: jest.fn()
    };
    next = jest.fn();
  });
 
  it('calls next with valid token', async () => {
    req.headers.authorization = 'Bearer valid-token';
    verifyToken.mockResolvedValue({ userId: 1 });
 
    await authMiddleware(req, res, next);
 
    expect(req.user).toEqual({ userId: 1 });
    expect(next).toHaveBeenCalled();
  });
 
  it('returns 401 without token', async () => {
    await authMiddleware(req, res, next);
 
    expect(res.status).toHaveBeenCalledWith(401);
    expect(next).not.toHaveBeenCalled();
  });
 
  it('returns 401 with invalid token', async () => {
    req.headers.authorization = 'Bearer invalid-token';
    verifyToken.mockRejectedValue(new Error('Invalid token'));
 
    await authMiddleware(req, res, next);
 
    expect(res.status).toHaveBeenCalledWith(401);
  });
});

How do you test CRUD operations?

Testing CRUD operations involves testing each operation (Create, Read, Update, Delete) along with error cases like validation failures and not-found scenarios.

// Using Supertest
import request from 'supertest';
import app from '../app';
import { db } from '../database';
 
describe('Users API', () => {
  beforeEach(async () => {
    await db.users.deleteAll();
  });
 
  describe('GET /api/users/:id', () => {
    it('returns user by ID', async () => {
      const user = await db.users.create({
        name: 'Alice',
        email: 'alice@example.com'
      });
 
      const response = await request(app)
        .get(`/api/users/${user.id}`)
        .expect(200);
 
      expect(response.body).toMatchObject({
        id: user.id,
        name: 'Alice',
        email: 'alice@example.com'
      });
    });
 
    it('returns 404 for non-existent user', async () => {
      await request(app)
        .get('/api/users/99999')
        .expect(404);
    });
  });
 
  describe('POST /api/users', () => {
    it('creates user with valid data', async () => {
      const response = await request(app)
        .post('/api/users')
        .send({ name: 'Bob', email: 'bob@example.com' })
        .expect(201);
 
      expect(response.body.id).toBeDefined();
 
      // Verify in database
      const user = await db.users.findById(response.body.id);
      expect(user.name).toBe('Bob');
    });
 
    it('validates required fields', async () => {
      const response = await request(app)
        .post('/api/users')
        .send({ name: 'Bob' }) // Missing email
        .expect(400);
 
      expect(response.body.errors).toContainEqual(
        expect.objectContaining({ field: 'email' })
      );
    });
  });
});

What is contract testing?

Contract testing ensures that API producers and consumers agree on the API contract. It's particularly useful in microservices architectures where services evolve independently.

// Using Pact for contract testing
import { Pact } from '@pact-foundation/pact';
 
const provider = new Pact({
  consumer: 'Frontend',
  provider: 'UserService',
});
 
describe('User API Contract', () => {
  beforeAll(() => provider.setup());
  afterAll(() => provider.finalize());
 
  it('returns user details', async () => {
    // Define expected interaction
    await provider.addInteraction({
      state: 'user 1 exists',
      uponReceiving: 'a request for user 1',
      withRequest: {
        method: 'GET',
        path: '/api/users/1',
      },
      willRespondWith: {
        status: 200,
        body: {
          id: 1,
          name: Matchers.string('Alice'),
          email: Matchers.email(),
        },
      },
    });
 
    // Make request to mock provider
    const response = await fetchUser(1);
 
    expect(response.name).toBeDefined();
 
    await provider.verify();
  });
});

Testing Best Practices Questions

These questions test your understanding of testing organization, coverage, and methodology.

How should you organize tests in a project?

Tests should be organized close to the code they test, with clear separation between unit, integration, and E2E tests. Co-locating tests with source files makes them easier to maintain.

src/
├── components/
│   ├── Button/
│   │   ├── Button.tsx
│   │   ├── Button.test.tsx     # Co-located tests
│   │   └── Button.stories.tsx
│   └── ...
├── hooks/
│   ├── useAuth.ts
│   └── useAuth.test.ts
├── services/
│   ├── api.ts
│   └── api.test.ts
└── __tests__/                   # Integration tests
    └── checkout.integration.test.ts

e2e/                             # E2E tests separate
├── checkout.spec.ts
└── auth.spec.ts

What naming conventions should tests follow?

Clear naming conventions make tests self-documenting. The describe-it pattern creates readable test output that explains what the code should do.

// Describe the unit under test
describe('ShoppingCart', () => {
  // Describe the scenario or method
  describe('addItem', () => {
    // State what should happen
    it('adds item to empty cart', () => {});
    it('increases quantity for existing item', () => {});
    it('throws error for invalid item', () => {});
  });
 
  describe('when cart has items', () => {
    it('calculates total correctly', () => {});
    it('applies discount codes', () => {});
  });
});
 
// Alternative: behavior-driven naming
it('should display error message when login fails', () => {});
it('should redirect to dashboard after successful login', () => {});

How do you configure code coverage?

Code coverage configuration should set different thresholds for different parts of your codebase. Critical business logic deserves higher coverage than boilerplate code.

// jest.config.js
module.exports = {
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/*.d.ts',
    '!src/index.tsx',
    '!src/**/*.stories.{js,jsx,ts,tsx}',
  ],
  coverageThreshold: {
    global: {
      branches: 70,
      functions: 70,
      lines: 80,
      statements: 80,
    },
    './src/utils/': {  // Higher threshold for critical code
      branches: 90,
      functions: 90,
      lines: 90,
    },
  },
};

Coverage metrics:

  • Lines: Percentage of lines executed
  • Branches: Percentage of if/else paths taken
  • Functions: Percentage of functions called
  • Statements: Percentage of statements executed

High coverage doesn't mean good tests. Focus on testing behavior, not achieving numbers.

When should you use TDD vs test-after?

TDD (Test-Driven Development) works well when you have clear requirements and need to ensure good design. Test-after development works better for exploratory work where requirements are still evolving.

TDD (Red-Green-Refactor):

1. Write failing test (RED)
2. Write minimal code to pass (GREEN)
3. Refactor while tests pass (REFACTOR)
4. Repeat
// 1. RED - Write test first
it('reverses a string', () => {
  expect(reverse('hello')).toBe('olleh');
});
// Test fails - function doesn't exist
 
// 2. GREEN - Minimal implementation
function reverse(str) {
  return str.split('').reverse().join('');
}
// Test passes
 
// 3. REFACTOR - Improve if needed
// (In this case, implementation is fine)

When TDD works well:

  • Clear requirements
  • Complex business logic
  • Bug fixes (write test that fails, then fix)
  • Refactoring existing code

When test-after works:

  • Exploratory development
  • UI/UX work
  • Prototyping
  • Learning new frameworks

Interview Challenge Questions

These questions simulate real interview scenarios.

How would you test this function?

When asked to test a function in an interview, demonstrate a systematic approach: identify the happy path, edge cases, error conditions, and side effects.

// Given this function
async function processOrder(order, paymentService, inventoryService) {
  // Validate order
  if (!order.items?.length) {
    throw new Error('Order must have items');
  }
 
  // Check inventory
  for (const item of order.items) {
    const available = await inventoryService.check(item.productId);
    if (available < item.quantity) {
      throw new Error(`Insufficient inventory for ${item.productId}`);
    }
  }
 
  // Process payment
  const payment = await paymentService.charge(order.total);
  if (!payment.success) {
    throw new Error('Payment failed');
  }
 
  // Reserve inventory
  for (const item of order.items) {
    await inventoryService.reserve(item.productId, item.quantity);
  }
 
  return { orderId: generateId(), payment };
}

Answer approach:

describe('processOrder', () => {
  let mockPaymentService;
  let mockInventoryService;
 
  beforeEach(() => {
    mockPaymentService = {
      charge: jest.fn().mockResolvedValue({ success: true })
    };
    mockInventoryService = {
      check: jest.fn().mockResolvedValue(100),
      reserve: jest.fn().mockResolvedValue(true)
    };
  });
 
  // Happy path
  it('processes valid order successfully', async () => {
    const order = { items: [{ productId: 1, quantity: 2 }], total: 50 };
 
    const result = await processOrder(order, mockPaymentService, mockInventoryService);
 
    expect(result.orderId).toBeDefined();
    expect(mockPaymentService.charge).toHaveBeenCalledWith(50);
    expect(mockInventoryService.reserve).toHaveBeenCalledWith(1, 2);
  });
 
  // Validation
  it('throws for empty order', async () => {
    await expect(processOrder({ items: [] }, mockPaymentService, mockInventoryService))
      .rejects.toThrow('Order must have items');
  });
 
  // Inventory check
  it('throws when inventory insufficient', async () => {
    mockInventoryService.check.mockResolvedValue(1); // Only 1 available
    const order = { items: [{ productId: 1, quantity: 5 }], total: 50 };
 
    await expect(processOrder(order, mockPaymentService, mockInventoryService))
      .rejects.toThrow('Insufficient inventory');
  });
 
  // Payment failure
  it('throws when payment fails', async () => {
    mockPaymentService.charge.mockResolvedValue({ success: false });
    const order = { items: [{ productId: 1, quantity: 1 }], total: 50 };
 
    await expect(processOrder(order, mockPaymentService, mockInventoryService))
      .rejects.toThrow('Payment failed');
 
    // Verify inventory not reserved
    expect(mockInventoryService.reserve).not.toHaveBeenCalled();
  });
});

How do you debug and fix flaky tests?

Flaky tests require a systematic debugging approach. Start by identifying the pattern of failure, then isolate the cause using debugging techniques.

Systematic approach:

  1. Identify the pattern: When does it fail? CI only? Certain times?

  2. Common causes:

    • Timing: Add proper waits, not arbitrary sleeps
    • Shared state: Isolate tests, reset between runs
    • Order dependency: Tests shouldn't depend on run order
    • External services: Mock or stabilize them
    • Race conditions: Fix async handling
  3. Debug techniques:

    // Run single test repeatedly
    jest --testNamePattern="flaky test" --runInBand --verbose
     
    // Add debugging
    it('flaky test', async () => {
      console.log('State before:', await getState());
      // ... test
      console.log('State after:', await getState());
    });
  4. If unfixable: Consider if the test provides value. A consistently flaky test is worse than no test.


Quick Reference

Testing Libraries

PurposeLibrary
Test runnerJest, Vitest, Mocha
React testingReact Testing Library
E2ECypress, Playwright
API testingSupertest
MockingJest mocks, MSW
AssertionsJest, Chai
CoverageIstanbul (built into Jest)

Jest Matchers Cheat Sheet

// Equality
expect(x).toBe(y);           // Strict equality (===)
expect(x).toEqual(y);        // Deep equality
expect(x).toStrictEqual(y);  // Deep + type equality
 
// Truthiness
expect(x).toBeTruthy();
expect(x).toBeFalsy();
expect(x).toBeNull();
expect(x).toBeUndefined();
expect(x).toBeDefined();
 
// Numbers
expect(x).toBeGreaterThan(y);
expect(x).toBeLessThan(y);
expect(x).toBeCloseTo(0.3);  // For floating point
 
// Strings
expect(str).toMatch(/regex/);
expect(str).toContain('substring');
 
// Arrays/Objects
expect(arr).toContain(item);
expect(arr).toHaveLength(3);
expect(obj).toHaveProperty('key');
expect(obj).toMatchObject({ partial: 'match' });
 
// Exceptions
expect(() => fn()).toThrow();
expect(() => fn()).toThrow('message');
 
// Async
await expect(promise).resolves.toBe(value);
await expect(promise).rejects.toThrow();
 
// Mocks
expect(mock).toHaveBeenCalled();
expect(mock).toHaveBeenCalledWith(arg);
expect(mock).toHaveBeenCalledTimes(3);

Ready to ace your interview?

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

View PDF Guides