← Назад к вопросам

Зачем нужны тесты?

1.0 Junior🔥 112 комментариев
#JavaScript Core

Комментарии (2)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Зачем нужны тесты

Тесты — это не просто «проверка кода». Это инвестиция в качество, надежность и масштабируемость приложения. Хороший набор тестов позволяет разработчикам работать быстрее и увереннее.

Основные причины писать тесты

1. Ловить ошибки ДО того, как их найдут пользователи

// Функция кажется простой
function calculateTotal(items) {
  return items.reduce((sum, item) => sum + item.price, 0);
}

// Но без тестов можно не заметить эти проблемы:
calculateTotal(null); // TypeError, приложение упадет
calculateTotal([{ price: '10' }]); // '010' вместо 10
calculateTotal([{ price: -5 }]); // Отрицательная сумма

// С тестами ошибки выловятся сразу
test('calculateTotal works with valid items', () => {
  expect(calculateTotal([{ price: 10 }, { price: 20 }])).toBe(30);
});

test('calculateTotal handles edge cases', () => {
  expect(() => calculateTotal(null)).toThrow();
  expect(calculateTotal([])).toBe(0);
});

2. Рефакторинг с уверенностью

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

// Старый код
function getUser(id) {
  const users = [
    { id: 1, name: 'John' },
    { id: 2, name: 'Jane' }
  ];
  for (let i = 0; i < users.length; i++) {
    if (users[i].id === id) return users[i];
  }
  return null;
}

// Рефакторим на более чистый код
function getUser(id) {
  const users = [
    { id: 1, name: 'John' },
    { id: 2, name: 'Jane' }
  ];
  return users.find(u => u.id === id) || null;
}

// Тесты остаются теми же, но мы уверены, что функция работает
test('getUser returns correct user by id', () => {
  expect(getUser(1)).toEqual({ id: 1, name: 'John' });
  expect(getUser(999)).toBeNull();
});

3. Документация кода

Тесты показывают, как код должен использоваться и какие edge cases нужно обрабатывать. Это лучше, чем комментарии.

// Тесты = живая документация
test('UserForm submits with valid data', () => {
  render(<UserForm onSubmit={mockSubmit} />);
  fillForm({ name: 'John', email: 'john@example.com' });
  clickButton('Submit');
  expect(mockSubmit).toHaveBeenCalledWith({ name: 'John', email: 'john@example.com' });
});

test('UserForm shows error for invalid email', () => {
  render(<UserForm onSubmit={mockSubmit} />);
  fillForm({ name: 'John', email: 'invalid' });
  expect(screen.getByText('Invalid email')).toBeInTheDocument();
  expect(mockSubmit).not.toHaveBeenCalled();
});

// Из этих тестов ясно: что принимает компонент, как его использовать, какие ошибки обработаны

4. Предотвращение регрессии

Когда один баг исправлен, тест гарантирует, что он не вернется при будущих изменениях.

// Баг: функция ломалась при пустом массиве
// Исправили, написали тест
test('should not crash with empty items', () => {
  expect(() => processItems([])).not.toThrow();
  expect(processItems([])).toEqual([]);
});

// Теперь если кто-то случайно сломает это при рефакторинге,
// тест сразу покажет красную лампочку

5. Раньше ловишь проблемы с дизайном кода

Если функцию сложно тестировать, скорее всего её дизайн плохой.

// ПЛОХОЙ ДИЗАЙН: сложно тестировать
class UserManager {
  async fetchAndSaveUser(id) {
    const response = await fetch(`/api/users/${id}`);
    const user = await response.json();
    localStorage.setItem('currentUser', JSON.stringify(user));
    return user;
  }
}
// Проблемы: зависит от fetch API, localStorage, сеть

// ХОРОШИЙ ДИЗАЙН: легко тестировать
class UserManager {
  constructor(apiClient, storage) {
    this.apiClient = apiClient;
    this.storage = storage;
  }

  async fetchAndSaveUser(id) {
    const user = await this.apiClient.getUser(id);
    this.storage.save('currentUser', user);
    return user;
  }
}

// Теперь легко тестировать с mock объектами
test('fetchAndSaveUser saves user to storage', async () => {
  const mockApi = { getUser: jest.fn().mockResolvedValue({ id: 1, name: 'John' }) };
  const mockStorage = { save: jest.fn() };
  const manager = new UserManager(mockApi, mockStorage);
  
  await manager.fetchAndSaveUser(1);
  
  expect(mockStorage.save).toHaveBeenCalledWith('currentUser', { id: 1, name: 'John' });
});

6. Экономия денег долгосрочно

Найти и исправить баг на production дороже, чем написать один тест, который его поймает.

Сценарий 1: Баг попал на production
- Пользователи потеряли данные
- Нужна срочная горячая правка
- Репутация компании пострадала
- Стоимость: $1000+

Сценарий 2: Тест поймал баг
- Разработчик видит ошибку в CI
- Исправляет на месте
- Стоимость: 5 минут разработчика

7. Уверенность и меньше стресса

Когда покрытие тестами хорошее, разработчики уверены в своем коде.

// Без тестов: страх перед изменениями
// "Если я это поменяю, что-нибудь сломаю?"
// Реакция: медленно, осторожно, раздражение

// С тестами: уверенность
// "Если я это поменяю и тесты зеленые, код работает"
// Реакция: быстро, смело, профессионально

Типы тестов и их назначение

1. Unit тесты (изолированные части)

// Тестируем одну функцию в изоляции
test('calculateDiscount returns correct value', () => {
  expect(calculateDiscount(100, 0.1)).toBe(10);
  expect(calculateDiscount(0, 0.5)).toBe(0);
});

Зачем: Ловить ошибки в логике на раннем этапе (быстро, дешево).

2. Integration тесты (несколько компонентов вместе)

// Тестируем форму + API + стейт вместе
test('form submission sends correct API request', async () => {
  render(<UserForm onSubmit={submitUser} />);
  fillForm({ name: 'John' });
  click('Submit');
  
  await waitFor(() => {
    expect(fetch).toHaveBeenCalledWith('/api/users', {
      method: 'POST',
      body: JSON.stringify({ name: 'John' })
    });
  });
});

Зачем: Убедиться, что части работают вместе (ближе к реальности).

3. E2E тесты (как реальный пользователь)

// Тестируем весь сценарий: открыть сайт -> заполнить форму -> отправить
test('user can register and login', async () => {
  await page.goto('https://app.com/register');
  await page.fill('[name=email]', 'john@example.com');
  await page.fill('[name=password]', 'secure123');
  await page.click('button[type=submit]');
  
  await page.goto('https://app.com/login');
  await page.fill('[name=email]', 'john@example.com');
  await page.fill('[name=password]', 'secure123');
  await page.click('button[type=submit]');
  
  expect(page.url()).toContain('/dashboard');
});

Зачем: Проверить, что весь флоу работает как надо (медленнее, но очень верно).

Пирамида тестов

         /\       <- E2E тесты (несколько)
        /  \         Медленные, дорогие, но уверенные
       /____\
      /      \     <- Integration тесты (умеренное количество)
     /        \       Средней скорости и цены
    /___________\
   /             \ <- Unit тесты (много)
  /               \    Быстрые, дешевые, базовая проверка
 /___________________\

Правильное распределение:

  • 70% Unit тесты (быстро работают)
  • 20% Integration тесты
  • 10% E2E тесты (проверяют критичные пути)

React пример: полный цикл

// Компонент
function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

// Unit тест
test('Counter increments on button click', () => {
  render(<Counter />);
  expect(screen.getByText('Count: 0')).toBeInTheDocument();
  
  click(screen.getByText('Increment'));
  expect(screen.getByText('Count: 1')).toBeInTheDocument();
});

// Integration тест
test('Counter resets to 0', async () => {
  render(<Counter />);
  click(screen.getByText('Increment'));
  click(screen.getByText('Increment'));
  
  expect(screen.getByText('Count: 2')).toBeInTheDocument();
  
  click(screen.getByText('Reset'));
  expect(screen.getByText('Count: 0')).toBeInTheDocument();
});

Минимальный набор для production

// Покрытие тестами должно быть >= 80%
// Формула: (строки кода в тестах) / (все строки кода) >= 0.80

// Проверить покрытие
npm run test:coverage

// Результат
// File      | Statements | Branches | Functions | Lines
// ----------|-----------|---------|-----------|------
// All files |   85%     |  80%    |   88%     |  85%
// ----------|-----------|---------|-----------|------

Главный вывод

Тесты нужны для:

  1. Качества — меньше багов на production
  2. Скорости разработки — рефакторинг без страха
  3. Документации — как пользовать код
  4. Экономии — ловишь ошибки дешево
  5. Уверенности — спишь спокойно
  6. Архитектуры — плохо тестируемый код = плохой дизайн
  7. Масштабируемости — легче добавлять фичи

Если ты разработчик, пишущий код без тестов — ты работаешь на 20% эффективнее своего потенциала. Тесты — это суперсила разработчика.

Зачем нужны тесты? | PrepBro