Перейти к содержимому

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

Unit Tests

Тестируй: Не тестируй:
✅ Бизнес-логику ❌ Конструкторы без логики
✅ Граничные случаи ❌ Геттеры/сеттеры (простые)
✅ Обработку ошибок ❌ Внешние библиотеки
✅ Валидацию данных ❌ Фреймворк
✅ Трансформации данных ❌ Тривиальный код
math.js
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.js
describe('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);
});
});
validator.js
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.js
describe('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);
});
});
Cart.js
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.js
describe('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');
});
});
});
  1. Напиши unit тесты для функции sortBy(array, key, direction)
  2. Протестируй класс PaginatedList (данные, страница, размер страницы)
  3. Покрой все граничные случаи функции parseDate(str)
  • Unit тесты — изолированно, без зависимостей
  • Тестируй граничные случаи: null, пустые строки, отрицательные числа
  • beforeEach сбрасывает состояние между тестами
  • test.each — для параметрических тестов