Table of Contents
- Unit Testing Principles
- Testing Utilities & Helpers
- Business Logic Testing
- Error Handling & Validation
Unit Testing Principles
Unit tests focus on testing individual functions in isolation:Unit Test Scope: Test pure functions, utilities, and business logic without external dependencies like databases, APIs, or UI components.
Organize Tests by Function:Key Principles:
// src/lib/__tests__/crypto.test.ts
import { describe, it, expect } from 'vitest'
import { hashPassword, verifyPassword, generateApiKey } from '../crypto'
describe('Crypto Utilities', () => {
describe('hashPassword', () => {
it('should hash password with bcrypt format', async () => {
const password = 'TestPassword123!'
const hash = await hashPassword(password)
expect(hash).not.toBe(password)
expect(hash).toMatch(/^\$2[aby]?\$/) // bcrypt format
expect(hash.length).toBeGreaterThan(50)
})
it('should generate different hashes for same password', async () => {
const password = 'TestPassword123!'
const hash1 = await hashPassword(password)
const hash2 = await hashPassword(password)
expect(hash1).not.toBe(hash2) // Salt should make them different
})
})
describe('verifyPassword', () => {
it('should verify correct password', async () => {
const password = 'TestPassword123!'
const hash = await hashPassword(password)
const isValid = await verifyPassword(password, hash)
expect(isValid).toBe(true)
})
it('should reject incorrect password', async () => {
const password = 'TestPassword123!'
const wrongPassword = 'WrongPassword123!'
const hash = await hashPassword(password)
const isValid = await verifyPassword(wrongPassword, hash)
expect(isValid).toBe(false)
})
})
})- One Function Per Describe Block: Group related tests
- Descriptive Test Names: Clearly state what is being tested
- Arrange-Act-Assert: Setup, execute, verify pattern
- Test Edge Cases: Include boundary conditions and error cases
Testing Utilities & Helpers
Test utility functions and helper modules used throughout the application:Email and Input Validation:
// src/lib/__tests__/validation.test.ts
import { describe, it, expect } from 'vitest'
import {
validateEmail,
validatePassword,
validateUrl,
sanitizeInput,
isValidUUID,
} from '../validation'
describe('Email Validation', () => {
it('should accept valid email formats', () => {
const validEmails = [
'test@example.com',
'user.name@domain.co.uk',
'test+tag@gmail.com',
'a@b.co',
]
validEmails.forEach(email => {
expect(validateEmail(email)).toBe(true)
})
})
it('should reject invalid email formats', () => {
const invalidEmails = [
'invalid-email',
'@domain.com',
'test@',
'test.domain.com',
'',
null,
undefined,
]
invalidEmails.forEach(email => {
expect(validateEmail(email as string)).toBe(false)
})
})
})
describe('Password Validation', () => {
it('should enforce minimum requirements', () => {
expect(validatePassword('weak')).toBe(false) // Too short
expect(validatePassword('12345678')).toBe(false) // No letters
expect(validatePassword('password')).toBe(false) // No numbers
expect(validatePassword('Password123')).toBe(true) // Valid
expect(validatePassword('SuperStrong123!')).toBe(true) // Very strong
})
it('should return validation details', () => {
const result = validatePassword('weak')
expect(result).toMatchObject({
isValid: false,
requirements: {
minLength: false,
hasLetter: true,
hasNumber: false,
hasSpecialChar: false,
},
})
})
})
describe('URL Validation', () => {
it('should validate different URL formats', () => {
expect(validateUrl('https://example.com')).toBe(true)
expect(validateUrl('http://localhost:3000')).toBe(true)
expect(validateUrl('ftp://files.example.com')).toBe(true)
expect(validateUrl('not-a-url')).toBe(false)
expect(validateUrl('')).toBe(false)
})
})
describe('Input Sanitization', () => {
it('should remove dangerous content', () => {
expect(sanitizeInput('<script>alert("xss")</script>')).toBe('alert("xss")')
expect(sanitizeInput('Hello <b>world</b>')).toBe('Hello world')
expect(sanitizeInput('Safe text')).toBe('Safe text')
})
it('should preserve safe formatting', () => {
const safeHtml = '<p>Hello <em>world</em></p>'
expect(sanitizeInput(safeHtml, { allowedTags: ['p', 'em'] })).toBe(safeHtml)
})
})
describe('UUID Validation', () => {
it('should validate UUID format', () => {
expect(isValidUUID('123e4567-e89b-12d3-a456-426614174000')).toBe(true)
expect(isValidUUID('550e8400-e29b-41d4-a716-446655440000')).toBe(true)
expect(isValidUUID('invalid-uuid')).toBe(false)
expect(isValidUUID('123')).toBe(false)
})
})Business Logic Testing
Test core business logic and calculation functions:Financial and Mathematical Calculations:
// src/lib/__tests__/calculations.test.ts
import { describe, it, expect } from 'vitest'
import {
calculateTax,
calculateDiscount,
calculateSubscriptionPrice,
calculateProration,
compoundInterest,
} from '../calculations'
describe('Tax Calculations', () => {
it('should calculate tax correctly', () => {
expect(calculateTax(100, 0.2)).toBe(20)
expect(calculateTax(1000, 0.15)).toBe(150)
expect(calculateTax(0, 0.2)).toBe(0)
})
it('should handle edge cases', () => {
expect(calculateTax(100, 0)).toBe(0)
expect(calculateTax(-100, 0.2)).toBe(-20) // Negative amounts
})
it('should round to correct decimal places', () => {
expect(calculateTax(33.33, 0.333)).toBe(11.11) // Rounded to 2 decimals
})
})
describe('Discount Calculations', () => {
it('should calculate percentage discount', () => {
expect(calculateDiscount(100, 0.2)).toBe(80) // 20% off
expect(calculateDiscount(50, 0.5)).toBe(25) // 50% off
})
it('should calculate fixed amount discount', () => {
expect(calculateDiscount(100, 10, 'fixed')).toBe(90)
expect(calculateDiscount(50, 60, 'fixed')).toBe(0) // Can't go below 0
})
it('should handle multiple discounts', () => {
const discounts = [
{ type: 'percentage', value: 0.1 }, // 10% off
{ type: 'fixed', value: 5 }, // $5 off
]
expect(calculateDiscount(100, discounts)).toBe(85) // $100 -> $90 -> $85
})
})
describe('Subscription Pricing', () => {
it('should calculate monthly pricing', () => {
const plan = { basePrice: 1000, features: { seats: 5 }, interval: 'month' }
expect(calculateSubscriptionPrice(plan)).toBe(1000)
})
it('should apply annual discount', () => {
const plan = { basePrice: 1000, interval: 'year', annualDiscount: 0.2 }
expect(calculateSubscriptionPrice(plan)).toBe(9600) // 12 * 1000 * 0.8
})
it('should calculate per-seat pricing', () => {
const plan = { basePrice: 1000, perSeat: 200 }
expect(calculateSubscriptionPrice(plan, { seats: 5 })).toBe(2000) // 1000 + (5 * 200)
})
})
describe('Proration Calculations', () => {
it('should calculate prorated amount for partial period', () => {
const startDate = new Date('2024-01-15')
const endDate = new Date('2024-02-01')
const monthlyPrice = 1000
const prorated = calculateProration(monthlyPrice, startDate, endDate)
expect(prorated).toBe(548) // ~17 days of 31-day month
})
it('should handle full month', () => {
const startDate = new Date('2024-01-01')
const endDate = new Date('2024-02-01')
const monthlyPrice = 1000
const prorated = calculateProration(monthlyPrice, startDate, endDate)
expect(prorated).toBe(1000)
})
})Error Handling & Validation
Test error conditions and validation logic:Input Validation and Sanitization:
// src/lib/__tests__/input-validation.test.ts
import { describe, it, expect } from 'vitest'
import {
validateUserInput,
sanitizeUserInput,
ValidationError,
validateSchema,
} from '../input-validation'
describe('User Input Validation', () => {
it('should validate required fields', () => {
const input = { name: '', email: 'test@example.com' }
const result = validateUserInput(input)
expect(result.isValid).toBe(false)
expect(result.errors).toContain('name is required')
})
it('should validate field formats', () => {
const input = { name: 'John Doe', email: 'invalid-email' }
const result = validateUserInput(input)
expect(result.isValid).toBe(false)
expect(result.errors).toContain('email format is invalid')
})
it('should validate field lengths', () => {
const input = {
name: 'J', // Too short
email: 'test@example.com',
password: '1234567890123456789012345678901234567890123456789012345678901234567890' // Too long
}
const result = validateUserInput(input)
expect(result.isValid).toBe(false)
expect(result.errors).toContain('name must be at least 2 characters')
expect(result.errors).toContain('password must be less than 50 characters')
})
it('should accept valid input', () => {
const input = {
name: 'John Doe',
email: 'john@example.com',
password: 'SecurePassword123!'
}
const result = validateUserInput(input)
expect(result.isValid).toBe(true)
expect(result.errors).toHaveLength(0)
})
})
describe('Input Sanitization', () => {
it('should remove dangerous scripts', () => {
const input = '<script>alert("xss")</script>Hello World'
const sanitized = sanitizeUserInput(input)
expect(sanitized).toBe('Hello World')
expect(sanitized).not.toContain('<script>')
})
it('should preserve safe content', () => {
const input = 'Hello <em>world</em> with <strong>formatting</strong>'
const sanitized = sanitizeUserInput(input, { allowedTags: ['em', 'strong'] })
expect(sanitized).toBe(input)
})
it('should trim whitespace', () => {
const input = ' Hello World '
const sanitized = sanitizeUserInput(input)
expect(sanitized).toBe('Hello World')
})
})
describe('Schema Validation', () => {
const userSchema = {
name: { type: 'string', required: true, minLength: 2 },
email: { type: 'email', required: true },
age: { type: 'number', min: 0, max: 150 },
role: { type: 'enum', values: ['user', 'admin', 'moderator'] },
}
it('should validate against schema', () => {
const validData = {
name: 'John Doe',
email: 'john@example.com',
age: 30,
role: 'user',
}
const result = validateSchema(validData, userSchema)
expect(result.isValid).toBe(true)
})
it('should detect schema violations', () => {
const invalidData = {
name: 'J', // Too short
email: 'invalid-email',
age: -5, // Below minimum
role: 'invalid-role',
}
const result = validateSchema(invalidData, userSchema)
expect(result.isValid).toBe(false)
expect(result.errors).toHaveLength(4)
})
it('should handle missing required fields', () => {
const incompleteData = { name: 'John Doe' }
const result = validateSchema(incompleteData, userSchema)
expect(result.isValid).toBe(false)
expect(result.errors).toContain('email is required')
})
})Best Practices Summary
Unit Testing Excellence:
- Test Pure Functions: Focus on functions without side effects
- Clear Test Names: Describe exactly what is being tested
- Fast Execution: Unit tests should run quickly in isolation
- Edge Cases: Test boundary conditions and error scenarios
Key Principles:
- Isolation: Each test should be independent and not rely on external state
- Deterministic: Tests should produce the same result every time
- Fast: Unit tests should execute quickly (< 10ms per test)
- Focused: One assertion per test, testing one specific behavior
- Readable: Tests should serve as documentation of the function's behavior
Common Patterns:
// ✅ Good unit test structure
describe('FunctionName', () => {
describe('when input is valid', () => {
it('should return expected result', () => {
const result = functionName(validInput)
expect(result).toBe(expectedOutput)
})
})
describe('when input is invalid', () => {
it('should throw ValidationError', () => {
expect(() => functionName(invalidInput)).toThrow(ValidationError)
})
})
})