Testing questions reveal how you reason about risk, feedback, and evidence. This guide contains exactly 26 interview questions; tool examples reflect the Jest 30, current Vitest, Playwright, Testing Library, MSW, Testcontainers, Cypress, and Pact documentation available in September 2026.
Table of Contents
- Testing Fundamentals Questions
- Unit Testing Questions
- Integration Testing Questions
- E2E Testing Questions
- React/Frontend Testing Questions
- API/Backend Testing Questions
- Testing Best Practices Questions
- Interview Challenge Questions
- 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 memorable heuristics, not competing laws or prescribed percentages. Both argue for feedback below full E2E; they emphasize different portfolios and vocabulary.
Testing Pyramid (traditional):
flowchart TB
subgraph PYRAMID["Testing Pyramid"]
E2E["Critical end-to-end journeys"]
INT["Boundary and integration tests"]
UNIT["Focused unit tests"]
end
UNIT --> INT --> E2E
style E2E fill:#dc2626,color:#ffffff
style INT fill:#f59e0b,color:#000000
style UNIT fill:#22c55e,color:#ffffffTesting 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:#ffffffThe trophy adds static analysis and emphasizes tests that integrate several application units. Neither shape tells you whether to run a real database, browser, provider sandbox, accessibility engine, migration, queue, or contract verification—the actual failure modes do.
Build a portfolio from risks: pure calculation errors need focused examples and properties; schema/query/transaction risks need the real database engine; consumer-provider drift needs contract verification; browser compatibility and critical journeys need real browsers. Track feedback time, diagnostic quality, flake rate, and escaped defects, then rebalance.
What makes a good test?
FIRST—Fast, Isolated, Repeatable, Self-validating, and Timely—is a useful mnemonic, not an absolute definition. A migration or recovery test can be slow and still essential. The stronger standard is that a test has a clear risk, controls nondeterminism, gives actionable failure evidence, and is cheap enough for its execution tier.
FIRST principles:
- Fast: Appropriate to the feedback tier and scheduled frequency
- Isolated: Parallel-safe state or explicit serialized ownership
- 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)
Usually avoid:
- Third-party library internals
- Language features
- Trivial getters/setters unless generated/security/language behavior is a risk
- Implementation details (test behavior, not how)
How do testing levels trade off speed vs confidence?
Scope influences cost and fidelity, but confidence is risk-specific. A slow E2E happy-path test gives little confidence in a rounding algorithm; a small property test can give much more. Conversely, a unit mock cannot prove SQL isolation or browser focus behavior.
| Scope | Strong evidence for | Common blind spot |
|---|---|---|
| Unit | Algorithms, domain rules, edge cases, state machines | Runtime and dependency integration |
| Component | UI behavior with controlled boundaries | Full browser/backend/deployment behavior |
| Integration | Database, queue, filesystem, adapter, framework semantics | Whole user journey and production topology |
| Contract | Consumer/provider compatibility | Provider implementation and runtime behavior |
| E2E | Selected journeys through a representative stack | Exhaustive paths and easy fault localization |
Select the smallest scope that can expose the named failure, then add higher-fidelity tests only where they buy distinct evidence.
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?
Mock, stub, and spy terminology varies between libraries. A stub returns controlled values; a spy records calls, often while preserving or replacing behavior; a mock commonly combines controlled behavior with interaction expectations. State which meaning you use and prefer assertions on observable outcomes over incidental call order.
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| Use a double when | Use the real dependency when |
|---|---|
| Testing caller behavior for timeout/error variants | Database/query/transaction semantics are the risk |
| A third-party call would be unsafe or costly | Filesystem, broker, cache, or browser behavior matters |
| Time/randomness needs a controlled seam | A lightweight collaborator adds useful fidelity cheaply |
| The boundary has a separately verified contract | The double would only restate the implementation |
How do you test async code?
Return or await every promise so the runner owns completion. Use fake timers only when time is an intentional dependency; advancing a clock is not the same as flushing every promise or I/O queue. Restore global timer state in cleanup so one test cannot contaminate another.
// 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();
try {
const promise = retryWithDelay(mockFn, 3, 1000);
await jest.advanceTimersByTimeAsync(3000);
await promise;
expect(mockFn).toHaveBeenCalledTimes(3);
} finally {
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?
If SQL dialect, constraints, migrations, transactions, indexes, collation, or driver behavior matters, test the same database engine and compatible version used in production. Isolation options include a disposable container/database per suite or worker, unique schemas, deterministic fixtures, and carefully designed transaction rollback. A test transaction can hide commit hooks or prevent code from opening independent transactions, so choose isolation around the behavior under test.
Option 1: Test database
// Use separate test database
const config = {
test: {
database: 'myapp_test',
// ... other config
}
};
beforeAll(async () => {
await db.migrate.latest();
});
beforeEach(async () => {
await resetOwnedTables();
await seedScenarioFixtures();
});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();
});A substitute is a different test boundary:
// Fast component substitute, not proof that PostgreSQL/MySQL behavior works
const db = new Database(':memory:');An in-memory repository or SQLite database can be useful for application tests, but it is not a “real database integration test” for PostgreSQL or MySQL. Differences in types, SQL, constraints, locking, isolation, extensions, and migrations can create false confidence. Keep at least one production-engine suite for those risks.
How do you write API integration tests?
API integration tests should cross the HTTP/framework boundary and, when relevant, use the production database engine. Supertest is one convenient in-process client for Node HTTP servers; a deployed black-box client provides different evidence. Test authentication/authorization, validation, media types, error schema, idempotency, concurrency, and persistence—not only happy-path JSON.
// 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 widely used browser-testing frameworks, but there is no universal winner. Compare the browsers and devices you must cover, debugging experience, execution model, language support, CI topology, accessibility tooling, and the team’s existing stack. A tool choice is evidence only when the tests exercise the risks that matter.
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.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Sign in' }).click();
});
test('completes purchase successfully', async ({ page }) => {
const firstProduct = page.getByTestId('product-card').first();
await firstProduct.getByRole('button', { name: 'Add to cart' }).click();
await page.getByRole('link', { name: 'Cart' }).click();
await page.getByRole('button', { name: 'Checkout' }).click();
await page.getByLabel('Address').fill('123 Main St');
await page.getByLabel('City').fill('New York');
await page.getByLabel('ZIP code').fill('10001');
await page.getByRole('button', { name: 'Place order' }).click();
await expect(page).toHaveURL(/order-confirmation/);
await expect(page.getByRole('heading', { name: /thank you/i })).toBeVisible();
});
});How do you fix flaky E2E tests?
Flaky tests change result without a relevant product change. Common causes include races, leaked state, unstable data, brittle selectors, resource pressure, and uncontrolled external systems. Reproduce and classify the failure before changing the test.
// Wait for a user-visible condition. Cypress retries the assertion.
cy.get('[data-testid="results"]').should('be.visible');
// Playwright locators and web-first assertions auto-wait and retry.
await expect(page.getByTestId('results')).toBeVisible();
await expect(page.getByRole('status')).toHaveText('5 results');
// If the response itself is the contract, tie the wait to the action.
const responsePromise = page.waitForResponse(
response => response.url().endsWith('/api/search') && response.ok()
);
await page.getByRole('button', { name: 'Search' }).click();
await responsePromise;Avoid fixed sleeps and broad “network idle” waits: both can be slower and still race. Framework assertion retries are synchronization. Retrying the whole failed test is different—it can collect traces or estimate flake rate, but a retry pass must not silently turn a nondeterministic test green.
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?
An E2E job needs a production-like build, a health-checked application, deterministic test data, isolated workers, secrets with least privilege, and useful failure artifacts. Pin or regularly update action/runtime versions through your dependency policy rather than copying versions from an interview answer.
# GitHub Actions
name: E2E Tests
on: [push, pull_request]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: npm ci
- name: Install browsers and OS dependencies
run: npx playwright install --with-deps
- name: Build, seed isolated data, and run E2E
run: npm run test:e2e:ci
- name: Upload traces and reports
uses: actions/upload-artifact@<approved-major>
if: ${{ !cancelled() }}
with:
name: e2e-artifacts
path: test-results/Here test:e2e:ci should start the app, poll a readiness endpoint, seed a worker-specific database, run tests, and always clean up. Shard only after tests are independent, and retain traces, videos, logs, and reports long enough to diagnose failures.
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 } 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?
For application code, prefer rendering the smallest component that uses the hook: the test then covers the public user-facing behavior. renderHook is useful when a reusable hook is itself the public library boundary. Direct state-changing calls may require act(); user-event and async query helpers already coordinate many updates, so follow the current React Testing Library guidance instead of wrapping everything mechanically.
// 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 intercepts requests at the network boundary, so the application keeps using its real client code. It is a strong default for component and integration tests, but it still simulates the server; verify important consumer/provider contracts and production integrations separately.
// Using MSW (Mock Service Worker)
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
http.get('/api/user', () =>
HttpResponse.json({ name: 'Alice', email: 'alice@example.com' })
)
);
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
it('displays user from API', async () => {
render(<UserProfile userId={1} />);
// Loading state
expect(screen.getByText(/loading/i)).toBeInTheDocument();
expect(await screen.findByText('Alice')).toBeInTheDocument();
});
it('handles API error', async () => {
// Override handler for this test
server.use(
http.get('/api/user', () => new HttpResponse(null, { status: 500 }))
);
render(<UserProfile userId={1} />);
expect(await screen.findByRole('alert')).toHaveTextContent(/error/i);
});When should you use snapshot testing?
Snapshot testing records serialized output and detects changes, not whether the new output is correct. It works best for small, stable structures whose diffs humans will review. Prefer behavioral assertions for user workflows and visual regression for layout; never approve a large snapshot update without understanding it.
// 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 oftenSnapshot 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?
Small unit tests can call middleware with request/response doubles to cover branching. Also keep HTTP-boundary tests for framework wiring: header parsing, middleware order, body serialization, error handling, and the guarantee that exactly one of next() or a response occurs.
// 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?
CRUD tests should cover the HTTP and persistence boundaries, not only one happy response per verb. Include authentication and authorization, validation, content negotiation, not-found and conflict behavior, unique constraints, partial updates, idempotency where promised, concurrent writes, transaction rollback, and durable database state.
// 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 checks whether two independently changing components agree at a boundary. A schema can validate shapes, but executable consumer-driven contracts can also capture the requests a real consumer sends and the responses it needs.
With Pact, the complete loop has two required sides:
- A consumer test drives the real client against a Pact mock and publishes the generated interaction.
- Provider verification replays that interaction against the real provider in each declared provider state.
- A broker or equivalent records compatibility so deployment checks can answer whether a version is safe to release.
A consumer mock passing alone does not prove compatibility. Avoid asserting every optional provider field, keep provider states deterministic, verify authorization/error variants that consumers depend on, and retain a smaller number of integration or E2E tests for concerns outside the contract boundary.
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?
Coverage reports show which instrumented statements, branches, functions, and lines executed. They do not show whether assertions would catch a fault, whether important inputs were chosen, or whether the requirement is correct. There is no universal target percentage.
// jest.config.js
module.exports = {
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
'!src/index.tsx',
'!src/**/*.stories.{js,jsx,ts,tsx}',
],
coverageProvider: 'v8',
coverageReporters: ['text', 'lcov'],
};Coverage metrics:
- Lines: Percentage of lines executed
- Branches: Percentage of if/else paths taken
- Functions: Percentage of functions called
- Statements: Percentage of statements executed
Use a report to find surprising gaps and review every exclusion. A risk-based floor or changed-code gate can prevent silent erosion, but choose and document it from defect history and criticality rather than copying 70/80/90. Pair coverage with error and boundary tests, property-based testing, mutation testing, contract evidence, and production signals. Note that Jest supports Babel and V8 coverage providers; collection adds runtime cost.
When should you use TDD vs test-after?
TDD is a short design-feedback loop, not a guarantee of good design or correctness. It is valuable when a small observable behavior can be stated before implementation—especially a regression, parser rule, calculation, or domain invariant. An exploratory spike, unfamiliar integration, visual interaction, or legacy seam may need discovery first; afterward, keep or rewrite tests around stable behavior rather than preserving implementation-shaped checks.
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)Choose the loop per uncertainty. TDD can help clarify ambiguous requirements through examples, while test-after can still produce excellent evidence if the test is independently designed and observed failing for the intended reason. For a bug fix, first reproduce the defect with a test whenever practical.
Interview Challenge Questions
These questions simulate real interview scenarios.
How would you test this function?
When asked to test a function in an interview, first inspect whether its contract and workflow are safe. Then identify invariants, happy paths, boundaries, failures, concurrency, retries, and observable 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 };
}A strong answer starts with design risks:
order.totalis trusted instead of calculated from authoritative prices.- Inventory is only checked, not reserved atomically, so another order can win the stock before reservation.
- Payment occurs before reservation; a later reservation failure can charge the customer without an order.
- There is no idempotency key, durable order state, compensation, or recovery for an unknown payment outcome.
- Sequential checks/reservations can partially mutate a multi-item order.
Unit tests should still cover empty/malformed items, quantities, duplicate product IDs, totals, unavailable stock, payment decline, reservation failure, and exceptions. Assert the returned state and durable outcomes first; use call assertions only where ordering is part of the contract.
Then add integration and fault-injection tests around the real transaction/workflow boundary: two orders racing for the last item, duplicate requests with one idempotency key, timeout after the provider accepted a charge, crash between steps, partial inventory failure, retry/reconciliation, and compensation. A browser happy path cannot establish those invariants, and mocks alone cannot prove database or provider semantics.
How do you debug and fix flaky tests?
Treat flakiness as a product or test-system defect. Preserve the failing seed, shard, worker, runtime, browser, clock, network evidence, logs, screenshots, and trace; reproduce with test retries disabled and repeated execution, then minimize the case.
Systematic approach:
-
Identify the pattern: CI only, specific order/seed/worker, resource pressure, time zone, browser, or time boundary?
-
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: Control them or test them in a separately classified environment
- Race conditions: Fix async handling
-
Debug techniques: run the same test repeatedly, randomize and record order, compare one vs many workers, capture traces, and instrument the causal state—not just extra sleeps.
// 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()); }); -
Containment: do not let a retry pass erase the signal. If quarantine is necessary, assign an owner and deadline, preserve an equivalent gate for the risk, and track flake rate. Delete a test only when its risk is obsolete or covered more effectively elsewhere.
Quick Reference
Testing libraries
| Purpose | Library |
|---|---|
| Test runner | Jest, Vitest, Mocha |
| React testing | React Testing Library |
| E2E | Cypress, Playwright |
| API testing | Supertest, framework-native clients, black-box HTTP clients |
| Mocking | Jest mocks, MSW |
| Assertions | Jest, Chai |
| Coverage | Jest/Vitest with V8 or Istanbul-compatible providers |
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);Frequently Asked Questions
What is the difference between unit, integration, and E2E tests?
The labels describe scope and boundary, not guaranteed speed or confidence. A unit test exercises one chosen unit and may use real lightweight collaborators. An integration test crosses a meaningful boundary such as application-to-database or consumer-to-provider adapter. An E2E test exercises a user or business flow through a deployed or representative stack. Choose the smallest scope that can expose the risk.
What is the testing pyramid and should I follow it?
The pyramid and testing trophy are heuristics for building a feedback portfolio, not universal quotas. Use static analysis, focused unit tests, boundary-aware integration and contract tests, a small set of critical browser journeys, and production checks according to failure risk, fidelity, execution cost, ownership, and diagnosis time. Measure escaped defects and suite feedback instead of conforming to a shape.
When should I mock dependencies in tests?
Use a test double when the boundary is outside the test's purpose, unsafe, expensive, nondeterministic, or difficult to trigger. Mock at stable owned interfaces or the network boundary, and assert outcomes before incidental calls. Use the real database engine, broker, filesystem, or browser when its semantics are the risk; an in-memory substitute can differ. Contract and provider tests keep doubles from drifting.
How do I fix flaky tests?
Reproduce with the same seed, shard, retries disabled, traces, clocks, and environment; then classify shared state, ordering, time, randomness, race, resource, network, selector, or product nondeterminism. Replace sleeps with event or web-first synchronization and isolate data. Retries may collect evidence but must not redefine a flaky pass as green. Quarantine only with an owner, deadline, and equivalent risk coverage.
What code coverage percentage should I aim for?
There is no defensible universal percentage. Line, branch, function, and statement coverage show what executed, not whether assertions detect faults or requirements are correct. Use reports to find surprising gaps, set risk-based floors or changed-code gates when helpful, review exclusions, and pair coverage with boundary, error, property, mutation, and production evidence. Never optimize a single total blindly.
Should I use TDD (Test-Driven Development)?
Use red-green-refactor when a small observable behavior can guide design and give fast feedback, especially for bugs and domain logic. Exploratory spikes, unfamiliar integration constraints, visual work, and legacy seams may need discovery before the test boundary is clear. Keep or rewrite tests around stable behavior after exploration; TDD is a design-feedback technique, not proof of correctness or a rule for every change.
Sources
- Testing Library: About Queries
- React Testing Library API
- Playwright: Locators
- Playwright: Auto-waiting and actionability
- Playwright: Assertions
- Playwright: Test retries
- Cypress: Retry-ability
- Cypress: Test isolation
- Mock Service Worker
- Testcontainers
- Jest 30 configuration and coverage
- Vitest browser mode
- Pact: How Pact works
- Pact JS: Provider verification
Related Articles
- Complete Frontend Developer Interview Guide - comprehensive preparation guide for frontend interviews
- React Hooks Interview Guide - Testing hooks with React Testing Library
- React Advanced Interview Guide - Component testing patterns
- Node.js Advanced Interview Guide - Testing async code
- REST API Interview Guide - API testing strategies
- CI/CD & GitHub Actions Interview Guide - Testing in pipelines
