TL;DR

  • Unit тестирование: Тестирование отдельных функций/методов изолированно
  • Зачем нужно: Ловит баги рано, позволяет безопасно рефакторить, документирует поведение кода
  • Ключевой принцип: Каждый тест проверяет ОДНУ вещь
  • Популярные фреймворки: Jest (JavaScript), pytest (Python), JUnit (Java)
  • Лучшая практика: Пиши тесты перед исправлением багов
  • ROI: Баги пойманные на уровне юнита стоят в 10-100x меньше

Время чтения: 10 минут

Unit тестирование — основа качества ПО. Оно тестирует отдельные куски кода изолированно, ловя баги до того как они распространятся на остальную систему.

Что такое Unit Тестирование?

Unit тестирование тестирует самые маленькие тестируемые части кода — функции и методы. Каждый юнит тест проверяет что конкретный кусок кода выдаёт ожидаемый результат для заданных входных данных.

Интеграционный тест: Login → API → База данных → Response
                     (Много компонентов работают вместе)

Unit тест: validateEmail("test@example.com") → true
           (Одна функция, изолированно)

Юнит тесты быстрые потому что не затрагивают базы данных, сеть или внешние сервисы.

Зачем Unit Тестирование Нужно

1. Ловит Баги Рано

Баги найденные при unit тестировании стоят намного меньше:

Этап обнаруженияОтносительная стоимость
Unit тестирование1x
Интеграционное тестирование10x
Системное тестирование40x
Production100x+

Найти баг в unit тесте — минуты. Найти его в production — дни.

2. Безопасный Рефакторинг

С unit тестами можно уверенно рефакторить:

// Оригинальная функция
function calculatePrice(price, quantity) {
  return price * quantity;
}

// Тесты защищают рефакторинг
test('calculates total price', () => {
  expect(calculatePrice(10, 3)).toBe(30);
});

// Безопасно рефакторить - тесты поймают ошибки
function calculatePrice(price, quantity, discount = 0) {
  return (price * quantity) * (1 - discount);
}

Тесты проверяют что функция всё ещё работает после изменений.

3. Документирует Поведение Кода

Тесты показывают как именно код должен использоваться:

// Тесты документируют ожидаемое поведение
test('returns empty array for null input', () => {
  expect(filterUsers(null)).toEqual([]);
});

test('filters users by active status', () => {
  const users = [
    { name: 'John', active: true },
    { name: 'Jane', active: false }
  ];
  expect(filterUsers(users)).toEqual([{ name: 'John', active: true }]);
});

Новые разработчики понимают поведение читая тесты.

4. Ускоряет Разработку

Парадоксально, написание тестов ускоряет разработку:

Без тестов:  Код → Ручной тест → Баг → Отладка → Фикс → Ручной тест
С тестами:  Код → Запуск тестов → Баг → Фикс → Запуск тестов (секунды)

Автоматизированные тесты дают мгновенную обратную связь.

Структура Unit Теста

Паттерн AAA

Каждый unit тест следует трём шагам:

test('adds two numbers correctly', () => {
  // Arrange: Подготовка тестовых данных
  const a = 5;
  const b = 3;

  // Act: Вызов функции
  const result = add(a, b);

  // Assert: Проверка результата
  expect(result).toBe(8);
});

Этот паттерн делает тесты читаемыми и поддерживаемыми.

Что Тестировать

  1. Happy path: Нормальное ожидаемое поведение
  2. Edge cases: Граничные условия
  3. Обработка ошибок: Невалидный ввод
describe('divide function', () => {
  // Happy path
  test('divides two numbers', () => {
    expect(divide(10, 2)).toBe(5);
  });

  // Edge case
  test('handles decimal results', () => {
    expect(divide(10, 3)).toBeCloseTo(3.33, 2);
  });

  // Обработка ошибок
  test('throws error for division by zero', () => {
    expect(() => divide(10, 0)).toThrow('Division by zero');
  });
});

Твой Первый Unit Тест

JavaScript (Jest)

// math.js
function add(a, b) {
  return a + b;
}

function multiply(a, b) {
  return a * b;
}

module.exports = { add, multiply };
// math.test.js
const { add, multiply } = require('./math');

describe('Math functions', () => {
  test('adds positive numbers', () => {
    expect(add(2, 3)).toBe(5);
  });

  test('adds negative numbers', () => {
    expect(add(-2, -3)).toBe(-5);
  });

  test('multiplies numbers', () => {
    expect(multiply(4, 5)).toBe(20);
  });

  test('multiplies by zero', () => {
    expect(multiply(4, 0)).toBe(0);
  });
});

Запуск: npm test.

Python (pytest)

# calculator.py
def add(a, b):
    return a + b

def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b
# test_calculator.py
import pytest
from calculator import add, divide

def test_add_positive():
    assert add(2, 3) == 5

def test_add_negative():
    assert add(-2, -3) == -5

def test_divide():
    assert divide(10, 2) == 5

def test_divide_by_zero():
    with pytest.raises(ValueError):
        divide(10, 0)

Запуск: pytest.

Java (JUnit)

// Calculator.java
public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }

    public int divide(int a, int b) {
        if (b == 0) {
            throw new IllegalArgumentException("Division by zero");
        }
        return a / b;
    }
}
// CalculatorTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class CalculatorTest {
    private Calculator calc = new Calculator();

    @Test
    void addPositiveNumbers() {
        assertEquals(5, calc.add(2, 3));
    }

    @Test
    void addNegativeNumbers() {
        assertEquals(-5, calc.add(-2, -3));
    }

    @Test
    void divideNumbers() {
        assertEquals(5, calc.divide(10, 2));
    }

    @Test
    void divideByZeroThrows() {
        assertThrows(IllegalArgumentException.class,
            () -> calc.divide(10, 0));
    }
}

Мокирование Зависимостей

Юнит тесты должны быть изолированы. Мокируй внешние зависимости:

// userService.js
async function getUser(id) {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}
// userService.test.js
jest.mock('node-fetch');

test('fetches user by id', async () => {
  // Мокируем ответ fetch
  fetch.mockResolvedValue({
    json: () => Promise.resolve({ id: 1, name: 'John' })
  });

  const user = await getUser(1);

  expect(user.name).toBe('John');
  expect(fetch).toHaveBeenCalledWith('/api/users/1');
});

Мокирование гарантирует что тесты быстрые и не зависят от внешних сервисов.

Покрытие Тестами

Понимание Покрытия

Покрытие измеряет какой процент кода тесты выполняют:

Line coverage:     Какой % строк выполнен
Branch coverage:   Какой % веток if/else протестирован
Function coverage: Какой % функций вызван

Практичные Цели Покрытия

Тип кодаЦелевое покрытие
Бизнес-логика80-90%
Утилиты90%+
UI компоненты60-70%
Сгенерированный код0% (пропускай)

Покрытие — это ориентир, не цель. Высокое покрытие не значит хорошие тесты.

// 100% покрытие но бесполезный тест
test('covers the function', () => {
  const result = complexCalculation(1, 2, 3);
  expect(result).toBeDefined(); // Слабая проверка
});

// Меньше покрытие но ценный тест
test('calculates discount correctly', () => {
  expect(calculateDiscount(100, 0.1)).toBe(90);
  expect(calculateDiscount(100, 0.5)).toBe(50);
});

Тестируй поведение, не цифры покрытия.

Лучшие Практики

1. Тестируй Одну Вещь

Каждый тест должен проверять одно поведение:

// Плохо: Тестирует несколько вещей
test('user validation', () => {
  expect(validateEmail('test@example.com')).toBe(true);
  expect(validateEmail('')).toBe(false);
  expect(validatePassword('abc')).toBe(false);
  expect(validatePassword('abcd1234')).toBe(true);
});

// Хорошо: Один тест на поведение
test('validates correct email', () => {
  expect(validateEmail('test@example.com')).toBe(true);
});

test('rejects empty email', () => {
  expect(validateEmail('')).toBe(false);
});

2. Используй Описательные Имена

Имена тестов должны описывать ожидаемое поведение:

// Плохие имена
test('test1', () => { ... });
test('validateEmail', () => { ... });

// Хорошие имена
test('rejects email without @ symbol', () => { ... });
test('accepts valid email with subdomain', () => { ... });

3. Держи Тесты Независимыми

Тесты не должны зависеть друг от друга:

// Плохо: Тесты зависят от общего состояния
let counter = 0;

test('increments counter', () => {
  counter++;
  expect(counter).toBe(1);
});

test('counter is one', () => {
  expect(counter).toBe(1); // Упадёт если первый тест не запустится
});

// Хорошо: Каждый тест независим
test('increments counter', () => {
  const counter = new Counter();
  counter.increment();
  expect(counter.value).toBe(1);
});

4. Пиши Тесты Перед Фиксом Багов

При исправлении бага сначала напиши тест который его воспроизводит:

// Баг: calculateTax возвращает NaN для отрицательных значений
// Шаг 1: Пишем падающий тест
test('handles negative values', () => {
  expect(calculateTax(-100)).toBe(0);
});

// Шаг 2: Исправляем баг
function calculateTax(amount) {
  if (amount < 0) return 0;
  return amount * 0.1;
}

// Шаг 3: Тест проходит, баг не вернётся

Unit Testing vs Другое Тестирование

ТипЧто тестируетСкоростьИзоляция
UnitОдну функциюСамая быстраяПолная
IntegrationВзаимодействие компонентовСредняяЧастичная
E2EПолные пользовательские сценарииСамая медленнаяНет

Unit тесты — основа пирамиды тестирования:

        /\
       /  \     E2E (мало)
      /----\
     /      \   Integration (немного)
    /--------\
   /          \ Unit (много)
  /____________\

Больше unit тестов, меньше интеграционных, ещё меньше E2E.

FAQ

Что такое unit тестирование?

Unit тестирование тестирует отдельные функции или методы в полной изоляции от остальной системы. Каждый тест фокусируется на одном маленьком куске кода — обычно одной функции — и проверяет что она выдаёт правильный результат для заданных входных данных. Ключевая характеристика — изоляция: unit тесты не затрагивают базы данных, API или другие внешние зависимости. Эта изоляция делает их быстрыми (миллисекунды) и надёжными.

Зачем нужно unit тестирование?

Unit тестирование ловит баги на самом раннем и дешёвом этапе разработки. Баг найденный при unit тестировании стоит примерно в 10-100 раз меньше чем найденный в production. Помимо поиска багов, unit тесты позволяют уверенно рефакторить (меняй код зная что тесты поймают ошибки), служат живой документацией (показывая как код должен себя вести) и ускоряют разработку через мгновенную обратную связь.

Что делает unit тест хорошим?

Хорошие unit тесты следуют принципам FIRST: Fast (быстрые, миллисекунды), Isolated (изолированные, без внешних зависимостей), Repeatable (повторяемые, тот же результат каждый раз), Self-validating (самопроверяющиеся, чёткий pass/fail), Timely (своевременные, написаны близко к коду). Они также следуют паттерну AAA: Arrange тестовые данные, Act вызовом функции, Assert ожидаемый результат. Каждый тест должен проверять одно конкретное поведение с описательным именем.

Какое покрытие тестами нужно?

Стремись к 70-80% покрытия на критичной бизнес-логике, 90%+ на утилитах, и 60-70% на UI компонентах. Однако покрытие — это ориентир, не цель. 100% покрытие не значит хорошие тесты — можно иметь полное покрытие со слабыми проверками. Фокусируйся на тестировании значимого поведения, а не на достижении цифр покрытия. Пропускай тестирование сгенерированного кода, геттеров/сеттеров и шаблонного кода фреймворка.

Смотрите также