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
- 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 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:#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 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 App | Recommendation |
|---|---|
| Heavy business logic | More unit tests (pyramid) |
| UI-heavy, user flows | More integration tests (trophy) |
| Complex algorithms | Unit tests for logic |
| CRUD application | Integration 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]| Level | Speed | Confidence | Maintenance |
|---|---|---|---|
| Unit | Fast | Lower | Low |
| Integration | Medium | Higher | Medium |
| E2E | Slow | Highest | High |
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| Mock | Don't Mock |
|---|---|
| External APIs | The code you're testing |
| Database calls | Pure functions |
| Time/dates | Simple dependencies |
| File system | In-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/screenshotsReact/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 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?
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:
-
Identify the pattern: When does it fail? CI only? Certain times?
-
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
-
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()); }); -
If unfixable: Consider if the test provides value. A consistently flaky test is worse than no test.
Quick Reference
Testing Libraries
| Purpose | Library |
|---|---|
| Test runner | Jest, Vitest, Mocha |
| React testing | React Testing Library |
| E2E | Cypress, Playwright |
| API testing | Supertest |
| Mocking | Jest mocks, MSW |
| Assertions | Jest, Chai |
| Coverage | Istanbul (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
- 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
