Testing Strategies Interview Guide: Unit, Integration, and E2E Testing

·17 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.

This guide covers testing strategies that come up in full-stack interviews: from unit tests to E2E, with practical examples in the JavaScript/TypeScript ecosystem.

Testing Fundamentals

The Testing Pyramid vs Testing Trophy

Testing Pyramid (traditional):

        /\
       /E2E\        Few, slow, high confidence
      /------\
     /Integration\   Some, medium speed
    /--------------\
   /   Unit Tests   \  Many, fast, low confidence per test
  /------------------\

Testing Trophy (Kent C. Dodds):

      ____
     / E2E \         Few
    /--------\
   |Integration|     Most tests here
   |----------|
    \ Unit  /        Some
     \----/
      Static         TypeScript, ESLint

Which to follow?

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

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.

What Makes a Good Test

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)

The Cost-Confidence Trade-off

                    High
                     |     * E2E Tests
        Confidence   |   * Integration
                     | * Unit
                    Low
                     +-------------------->
                    Fast              Slow
                         Speed

Every testing level trades something:

LevelSpeedConfidenceMaintenance
UnitFastLowerLow
IntegrationMediumHigherMedium
E2ESlowHighestHigh

Unit Testing

What Makes a Good Unit Test

Test one thing in isolation:

// 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');
  });
});

Arrange-Act-Assert Pattern

Structure every test the same way:

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'
  });
});

Mocking, Stubbing, and Spying

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

When to mock:

MockDon't Mock
External APIsThe code you're testing
Database callsPure functions
Time/datesSimple dependencies
File systemIn-memory alternatives

Testing Async Code

// 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

When Integration Tests Shine

Integration tests catch bugs that unit tests miss:

// 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');
  });
});

Testing with Real Databases

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:"
}

API Integration Tests

// 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');
  });
});

End-to-End Testing

Browser Automation Tools

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();
  });
});

Handling Flaky Tests

Common causes and fixes:

// 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');

E2E in CI/CD

# 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

Testing React/Frontend

Component Testing with React Testing Library

// 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'
    });
  });
});

Testing Hooks

// 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);
  });
});

Testing with Mocked API

// 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 to Use Snapshot Testing

// 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)

Testing APIs/Backend

Express API Testing

// 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' })
      );
    });
  });
});

Testing Middleware

// 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);
  });
});

Contract Testing Basics

Ensure API producer and consumer agree on the contract:

// 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

Test Organization

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

Naming Conventions

// 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', () => {});

Code Coverage

// 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

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

TDD vs Test-After

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

Common Interview Questions

"How would you test this function?"

// 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();
  });
});

"This test is flaky. How do you fix it?"

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);

Related Articles

This guide connects to the broader full-stack interview preparation:

Frontend Testing:

Backend Testing:

DevOps Integration:


Final Thoughts

Testing interviews assess your understanding of trade-offs, not just syntax. Key takeaways:

  1. Know the pyramid/trophy: Understand when each test level is appropriate
  2. Mock wisely: Don't over-mock, don't under-mock
  3. Write maintainable tests: Test behavior, not implementation
  4. Fix flaky tests: Or delete them—flaky tests erode trust
  5. Coverage is a tool: Not a goal

The best testers write tests that give confidence without slowing development. That balance is what interviewers want to see.

Ready to ace your interview?

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

View PDF Guides