Tags: testing, unit-testing, jest, vitest, tdd, mocks, assertions
Level: intermediate
TL;DR: Quick reference for core unit testing concepts, test doubles, isolation principles, and standard patterns for technical interviews.
Unit tests verify the smallest testable parts of an application in complete isolation.
Fast execution enables immediate feedback loops during active development and refactoring.
Deterministic behavior ensures tests yield identical results regardless of execution order or environment.
The Arrange-Act-Assert (AAA) pattern structures individual test cases clearly.
Test doubles replace external dependencies like databases, networks, and system clocks.
Mocks verify interactions and method invocations on dependencies.
Stubs provide predetermined canned responses to calls made during the test.
Code coverage measures executed lines, but high coverage does not guarantee thorough assertion quality.
Tests should validate public behavior and contracts, not private internal implementation details.
F.I.R.S.T. principles define effective tests: Fast, Independent, Repeatable, Self-validating, and Timely.
Q: What is the difference between a unit test and an integration test?R: A unit test verifies isolated business logic by mocking external dependencies. An integration test validates real interactions across multiple units, databases, or third-party services.
Q: When should you use a mock versus a stub?R: I use stubs to supply predictable inputs or state to the system under test. I use mocks when I need to verify that a specific side effect or method call occurred.
Q: How do you approach testing code that relies on private methods?R: I test private methods indirectly through the public interface. If a private method requires isolated testing, it often indicates an opportunity to extract a separate collaborator.
Q: What is Test-Driven Development (TDD) and its cycle?R: TDD follows Red-Green-Refactor: write a failing test first, write minimal code to make it pass, then refactor while keeping tests green.
Testing implementation details instead of behavior causes tests to break during harmless refactoring.
Shared mutable state between test cases causes flakiness and order-dependent failures.
Over-mocking dependencies can result in passing tests while real integration fails.
Asserting multiple unrelated concerns in a single unit test obscures failure causes.
Relying on real timers or network requests introduces latency and intermittent failures.
```import { describe, it, expect, vi } from 'vitest';
describe('applyDiscount', () => {
it('should apply percentage discount correctly', () => {
// Arrange
const originalPrice = 100;
const discountPercent = 20;
// Act
const result = applyDiscount(originalPrice, discountPercent);
// Assert
expect(result).toBe(80);
});
});
npm test -- --watch
npm test -- --coverage
```
[ ] Boundaries between spies, stubs, fakes, and mocks in the current test framework
[ ] Mocking asynchronous promises and rejected states cleanly
[x] Structuring tests using the Arrange-Act-Assert pattern
[x] Avoiding tight coupling to internal implementation details
Martin Fowler: Mocks Aren't Stubs
Kent Beck: Test-Driven Development by Example
Gerard Meszaros: xUnit Test Patterns