Testing & Verification
Tests are not just a safety net—they choose the architecture of your application. Master the paradigms of **Unit**, **Integration**, and **End-to-End** testing to build software that is inherently predictable and safe to refactor.
The Testing Pyramid
A balanced testing strategy follows the Pyramid model: high numbers of fast, cheap **Unit tests** at the base; a moderate layer of **Integration tests** for component communication; and a thin layer of **End-to-End (E2E) tests** that simulate real user journeys. This distribution ensures deep coverage without crippling build performance.
// 1. The Anatomy of a Unit Test (Vitest/Jest)
import { describe, it, expect } from 'vitest';
describe('Auth Service', () => {
it('should validate strong passwords', () => {
// Arrange
const password = "P@ssword123";
// Act
const isValid = validatePassword(password);
// Assert
expect(isValid).toBe(true);
});
});
// 2. Testing Edge Cases
it('should reject passwords shorter than 8 chars', () => {
expect(validatePassword("Short1!")).toBe(false);
});Asynchronous Verification
Modern JavaScript is heavily asynchronous. Testing `async/await` logic requires specific handling to ensure assertions are reached before the test process terminates. Using `await expect(...).rejects` is the professional standard for verifying error boundaries and network failures.
// Testing Asynchronous Logic
it('should fetch user profile from API', async () => {
const user = await githubService.fetchUser('octocat');
expect(user).toHaveProperty('login', 'octocat');
expect(user.public_repos).toBeGreaterThan(0);
});
// Handling Promise Rejections
it('should throw Unauthorized on 401 response', async () => {
await expect(authService.login('bad', 'creds'))
.rejects
.toThrow('Unauthorized');
});Mocking & Dependency Isolation
Unit tests should never touch actual databases or network APIs. Mocks and Spies allow you to isolate the "Subject Under Test" by replacing complex dependencies with controlled stubs. This ensures your tests are **Deterministic** (they pass/fail based solely on the logic, not the environment).
// Mocking External Dependencies
import { vi } from 'vitest';
it('should send an email after registration', async () => {
// 1. Create a "Spy" or "Mock"
const sendMailSpy = vi.spyOn(emailProvider, 'send');
// 2. Execute target logic
await registerUser({ email: 'test@example.com' });
// 3. Verify interaction
expect(sendMailSpy).toHaveBeenCalledWith(
expect.stringContaining('test@example.com')
);
});Technical Insight: TDD Lifecycle
**Test-Driven Development (TDD)** follows the **Red-Green-Refactor** cycle: Write a failing test for a new feature (**Red**), write the minimum code to make it pass (**Green**), and then clean up the implementation (**Refactor**). TDD ensures that every line of code is justified and that you maintain 100% test coverage for the business logic as you build.
Testing Best Practices:
- ✅ **AAA Pattern:** Organize tests into Arrange, Act, and Assert blocks.
- ✅ **Isolation:** Use `vi.mock()` or `jest.mock()` for external API boundaries.
- ✅ **Edge Cases:** Test null inputs, empty arrays, and boundary numbers.
- ✅ **Speed:** Unit tests should run in milliseconds; avoid heavy setup in them.
- ⌠**Anti-Pattern:** Avoid testing implementation details; test **results** and **behavior**.