6. Unit тесты: основы

Что тестировать в unit тестах?
Заголовок раздела «Что тестировать в unit тестах?»Тестируй: Не тестируй:✅ Бизнес-логику ❌ Конструкторы без логики✅ Граничные случаи ❌ Геттеры/сеттеры (простые)✅ Обработку ошибок ❌ Внешние библиотеки✅ Валидацию данных ❌ Фреймворк✅ Трансформации данных ❌ Тривиальный кодТестирование функций
Заголовок раздела «Тестирование функций»export function divide(a, b) { if (b === 0) throw new Error('Division by zero'); return a / b;}
export function clamp(value, min, max) { return Math.min(Math.max(value, min), max);}
export function formatCurrency(amount, locale = 'ru-RU', currency = 'RUB') { return new Intl.NumberFormat(locale, { style: 'currency', currency, }).format(amount);}
// math.test.jsdescribe('divide', () => { test('divides correctly', () => { expect(divide(10, 2)).toBe(5); expect(divide(7, 2)).toBe(3.5); });
test('throws on division by zero', () => { expect(() => divide(10, 0)).toThrow('Division by zero'); });
test('handles negative numbers', () => { expect(divide(-10, 2)).toBe(-5); expect(divide(10, -2)).toBe(-5); });});
describe('clamp', () => { test('returns value when within range', () => { expect(clamp(5, 1, 10)).toBe(5); });
test('clamps to min', () => { expect(clamp(-5, 0, 100)).toBe(0); });
test('clamps to max', () => { expect(clamp(200, 0, 100)).toBe(100); });});Граничные случаи
Заголовок раздела «Граничные случаи»export function validateEmail(email) { const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return re.test(String(email).toLowerCase());}
export function validatePassword(password) { if (!password || password.length < 8) return false; if (!/[A-Z]/.test(password)) return false; if (!/[0-9]/.test(password)) return false; return true;}
// validator.test.jsdescribe('validateEmail', () => { // Валидные email test.each([ ])('valid: %s', (email) => { expect(validateEmail(email)).toBe(true); });
// Невалидные email test.each([ '', null, undefined, 'notanemail', '@nodomain', 'no@', ])('invalid: %s', (email) => { expect(validateEmail(email)).toBe(false); });});
describe('validatePassword', () => { test('valid password', () => { expect(validatePassword('MyPass1!')).toBe(true); expect(validatePassword('Secret123')).toBe(true); });
test('too short', () => { expect(validatePassword('Ab1')).toBe(false); });
test('no uppercase', () => { expect(validatePassword('mypassword1')).toBe(false); });
test('no digits', () => { expect(validatePassword('MyPassword')).toBe(false); });
test('empty/null/undefined', () => { expect(validatePassword('')).toBe(false); expect(validatePassword(null)).toBe(false); expect(validatePassword(undefined)).toBe(false); });});Тестирование классов
Заголовок раздела «Тестирование классов»export class Cart { #items = []; #discount = 0;
get items() { return [...this.#items]; }
get subtotal() { return this.#items.reduce((sum, item) => sum + item.price * item.qty, 0); }
get total() { return this.subtotal * (1 - this.#discount / 100); }
addItem(item) { if (!item.id || !item.price || item.price <= 0) { throw new Error('Invalid item'); } const existing = this.#items.find(i => i.id === item.id); if (existing) { existing.qty += item.qty ?? 1; } else { this.#items.push({ ...item, qty: item.qty ?? 1 }); } }
removeItem(id) { this.#items = this.#items.filter(i => i.id !== id); }
applyDiscount(percent) { if (percent < 0 || percent > 100) throw new Error('Invalid discount'); this.#discount = percent; }
clear() { this.#items = []; }}
// Cart.test.jsdescribe('Cart', () => { let cart; const apple = { id: 1, name: 'Apple', price: 1.5, qty: 2 }; const bread = { id: 2, name: 'Bread', price: 2.0 };
beforeEach(() => { cart = new Cart(); });
describe('addItem', () => { test('adds new item', () => { cart.addItem(apple); expect(cart.items).toHaveLength(1); expect(cart.items[0]).toMatchObject({ id: 1, qty: 2 }); });
test('increases qty for existing item', () => { cart.addItem({ id: 1, price: 1.5, qty: 1 }); cart.addItem({ id: 1, price: 1.5, qty: 3 }); expect(cart.items[0].qty).toBe(4); });
test('throws for invalid item', () => { expect(() => cart.addItem({ price: -1 })).toThrow('Invalid item'); }); });
describe('total', () => { test('calculates subtotal', () => { cart.addItem(apple); // 1.5 * 2 = 3 cart.addItem(bread); // 2 * 1 = 2 expect(cart.subtotal).toBeCloseTo(5.0); });
test('applies discount', () => { cart.addItem({ id: 1, price: 100, qty: 1 }); cart.applyDiscount(20); expect(cart.total).toBe(80); });
test('throws for invalid discount', () => { expect(() => cart.applyDiscount(150)).toThrow('Invalid discount'); expect(() => cart.applyDiscount(-10)).toThrow('Invalid discount'); }); });});Практические задания
Заголовок раздела «Практические задания»- Напиши unit тесты для функции
sortBy(array, key, direction) - Протестируй класс
PaginatedList(данные, страница, размер страницы) - Покрой все граничные случаи функции
parseDate(str)
- Unit тесты — изолированно, без зависимостей
- Тестируй граничные случаи: null, пустые строки, отрицательные числа
- beforeEach сбрасывает состояние между тестами
- test.each — для параметрических тестов