Тестирование ошибок и edge cases

Тестирование ошибок и пограничных случаев — проверка поведения кода при невалидных входных данных, исключительных ситуациях и граничных значениях, когда система должна либо корректно обработать проблему, либо выбросить понятную ошибку.

Зачем нужно

Большинство багов в production возникает не в «happy path», а в исключительных ситуациях: пустой массив, null вместо объекта, сетевой сбой, невалидный JSON. Тестирование edge cases делает систему устойчивой к реальным условиям эксплуатации.

Где используется

  • Валидация и парсинг пользовательского ввода
  • Обработка сетевых ошибок и таймаутов
  • Граничные значения в алгоритмах (пустой массив, один элемент)
  • TypeScript — проверка путей с null/undefined

Основной контент

Тестирование выброса ошибок

// synchronous error
test('выбрасывает TypeError при null', () => {
  expect( => processUser(null)).toThrow(TypeError);
  expect( => processUser(null)).toThrow('User cannot be null');
});

// async error
test('выбрасывает ошибку при 404', async () => {
  await expect(fetchUser(999)).rejects.toThrow('Not found');
  await expect(fetchUser(999)).rejects.toBeInstanceOf(NotFoundError);
});

Edge cases — чеклист

// Для числовых функций
test('работает с нулём', () => expect(factorial(0)).toBe(1));
test('работает с отрицательным числом', () => {
  expect( => factorial(-1)).toThrow;
});
test('работает с очень большим числом', () => {
  expect(typeof factorial(20)).toBe('number');
});

// Для строковых функций
test('пустая строка', () => expect(capitalize('')).toBe(''));
test('строка из пробелов', () => expect(capitalize('   ')).toBe('   '));
test('Unicode строка', () => expect(capitalize('привет')).toBe('Привет'));
test('строка с числами', () => expect(capitalize('123abc')).toBe('123abc'));

// Для функций с массивами
test('пустой массив', () => expect(sum()).toBe(0));
test('один элемент', () => expect(sum([5])).toBe(5));
test('массив с NaN', () => expect(isNaN(sum([1, NaN]))).toBe(true));

// Для функций с объектами
test('null — выбрасывает ошибку', () => {
  expect( => getProperty(null, 'key')).toThrow;
});
test('undefined key — возвращает undefined', () => {
  expect(getProperty({}, 'nonexistent')).toBeUndefined();
});

Тестирование с toThrow

// Проверить что выбрасывается — разные варианты
expect( => fn).toThrow;                        // любая ошибка
expect( => fn).toThrow(Error);                   // тип ошибки
expect( => fn).toThrow('message');               // подстрока сообщения
expect( => fn).toThrow(/regex/);                 // регулярное выражение
expect( => fn).toThrowError(ValidationError);    // кастомный класс ошибки

Тестирование сетевых ошибок

jest.mock('./api');
import { fetchData } from './api';

test('обрабатывает сетевую ошибку', async () => {
  fetchData.mockRejectedValue(new Error('Network Error'));

  render(<DataComponent />);

  await screen.findByText('Ошибка загрузки. Попробуйте позже.');
  expect(screen.queryByText('Загрузка...')).not.toBeInTheDocument;
});

test('обрабатывает таймаут', async () => {
  fetchData.mockRejectedValue(new Error('Request timeout'));

  const result = await fetchWithRetry('/api/data', { retries: 3 });

  expect(fetchData).toHaveBeenCalledTimes(3); // 3 попытки
  expect(result).toBeNull();
});

expect.assertions — защита от "молчащих" тестов

test('async ошибка обязательно проверяется', async () => {
  expect.assertions(1); // тест упадёт если expect не вызван ни разу

  try {
    await fetchUser(null);
  } catch (error) {
    expect(error.message).toBe('Invalid user id');
  }
});

Частые ошибки

  • Нет теста на null/undefined — это самый частый источник runtime ошибок; всегда проверяй что происходит при null входе
  • toThrow для async без awaitexpect(asyncFn).toThrow не работает для Promise; используй rejects.toThrow
  • Тест ловит ошибку но не проверяетtry { ... } catch (e) {} без expect делает тест бессмысленным
  • Нет теста на граничные типыNaN, Infinity, -0, '' часто вызывают неожиданное поведение

Связанные темы

Ресурсы