Тестирование чистых функций
Чистая функция — функция без side effects, которая всегда возвращает одинаковый результат для одинаковых аргументов; она является идеальным кандидатом для unit-тестирования: не требует моков и изолирована по определению.
Зачем нужно
Чистые функции — самые простые в тестировании: нет зависимостей, нет моков, нет async. Тест — это просто expect(fn(input)).toBe(expected). Если стремиться к чистоте функций, код автоматически становится тестируемым.
Где используется
- Утилитарные функции (форматирование, парсинг, вычисления)
- Redux reducers (чистые по определению)
- Функциональные трансформации данных (map, filter, reduce pipeline)
- Валидаторы, конвертеры, генераторы
Основной контент
Определение чистой функции
// ЧИСТАЯ — нет side effects, детерминированная
function add(a: number, b: number): number {
return a + b;
}
function capitalize(str: string): string {
if (!str) return str;
return str[0].toUpperCase() + str.slice(1).toLowerCase();
}
// НЕ ЧИСТАЯ — изменяет внешнее состояние
let total = 0;
function addToTotal(value: number): void {
total += value; // side effect!
}
// НЕ ЧИСТАЯ — недетерминированная
function getTimestamp: number {
return Date.now(); // разный результат при каждом вызове
}
Тест чистой функции: минимум кода
// formatCurrency.test.ts
import { formatCurrency } from './formatCurrency';
describe('formatCurrency', () => {
test('форматирует рубли', () => {
expect(formatCurrency(1000, 'RUB')).toBe('1 000 ₽');
});
test('форматирует доллары', () => {
expect(formatCurrency(1500.5, 'USD')).toBe('$1,500.50');
});
test('обрабатывает ноль', () => {
expect(formatCurrency(0, 'RUB')).toBe('0 ₽');
});
test('обрабатывает отрицательные значения', () => {
expect(formatCurrency(-500, 'RUB')).toBe('-500 ₽');
});
});
Параметризованные тесты для чистых функций
// clamp.test.ts
import { clamp } from './clamp';
test.each([
[5, 0, 10, 5], // в диапазоне
[-5, 0, 10, 0], // меньше min
[15, 0, 10, 10], // больше max
[0, 0, 10, 0], // граница min
[10, 0, 10, 10], // граница max
])('clamp(%d, %d, %d) → %d', (value, min, max, expected) => {
expect(clamp(value, min, max)).toBe(expected);
});
Redux reducer — чистая функция
// counterReducer.test.ts
import { counterReducer } from './counterReducer';
const initialState = { count: 0 };
test('INCREMENT увеличивает count', () => {
const newState = counterReducer(initialState, { type: 'INCREMENT' });
expect(newState.count).toBe(1);
// Исходный state не изменился
expect(initialState.count).toBe(0);
});
test('не мутирует исходный state', () => {
const state = { count: 5 };
const newState = counterReducer(state, { type: 'DECREMENT' });
expect(newState).not.toBe(state); // новый объект
expect(state.count).toBe(5); // исходный не изменился
});
Тестирование pipeline трансформаций
// Функциональный pipeline
const processUsers = (users) =>
users
.filter((u) => u.active)
.map((u) => ({ ...u, fullName: `${u.firstName} ${u.lastName}` }))
.sort((a, b) => a.fullName.localeCompare(b.fullName));
test('фильтрует, маппит и сортирует пользователей', () => {
const input = [
{ firstName: 'Bob', lastName: 'Smith', active: true },
{ firstName: 'Alice', lastName: 'Jones', active: true },
{ firstName: 'Charlie', lastName: 'Brown', active: false },
];
const result = processUsers(input);
expect(result).toHaveLength(2);
expect(result[0].fullName).toBe('Alice Jones');
expect(result[1].fullName).toBe('Bob Smith');
});
Частые ошибки
- Тест мутации —
const arr = [3, 1, 2]; fn(arr); expect(arr).toEqual([1, 2, 3])— функция мутирует аргумент; чистая функция должна возвращать новый массив - Нет теста иммутабельности — всегда проверяй что исходный объект/массив не изменился:
expect(input).toEqual(originalInput) - Зависимость от Math.random или Date.now() — такие функции не чистые; для тестирования передавай seed или время как аргумент