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 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 |
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:
| Level | Speed | Confidence | Maintenance |
|---|---|---|---|
| Unit | Fast | Lower | Low |
| Integration | Medium | Higher | Medium |
| E2E | Slow | Highest | High |
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 upWhen to mock:
| Mock | Don't Mock |
|---|---|
| External APIs | The code you're testing |
| Database calls | Pure functions |
| Time/dates | Simple dependencies |
| File system | In-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/screenshotsTesting 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 oftenSnapshot 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:
-
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
This guide connects to the broader full-stack interview preparation:
Frontend Testing:
- React Hooks Interview Guide - Testing hooks
- React Advanced Interview Guide - Component patterns
Backend Testing:
- Node.js Advanced Interview Guide - Testing async code
- REST API Interview Guide - API testing strategies
DevOps Integration:
- CI/CD & GitHub Actions Interview Guide - Testing in pipelines
Final Thoughts
Testing interviews assess your understanding of trade-offs, not just syntax. Key takeaways:
- Know the pyramid/trophy: Understand when each test level is appropriate
- Mock wisely: Don't over-mock, don't under-mock
- Write maintainable tests: Test behavior, not implementation
- Fix flaky tests: Or delete them—flaky tests erode trust
- 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.
