Прежде чем писать тест, надо решить, что проверять. Принцип Testing Library краток: чем больше тест похож на реальное использование кода, тем больше он внушает доверия. Проверять надо наблюдаемое поведение - то, что видит вызывающая сторона: возвращаемое значение, ошибку, изменение на экране, - а не приватные детали устройства. Единица теста - не файл и не класс, а один осмысленный кусок поведения с входом и наблюдаемым выходом.
Проще всего это видно на чистой функции - зависящей только от аргументов и не трогающей ничего снаружи: ни времени, ни сети, ни глобальных переменных. calculateDelivery как раз такая: дали заказ - получили число. Чистота - подарок для теста: один вход всегда даёт один выход, и проверка сводится к таблице пар 'вход - ожидаемый результат'. С таких функций и стоит начинать - они учат сути проверки, не отвлекая на моки и таймеры.
Тело теста укладывается в три шага - Arrange-Act-Assert. Arrange собирает вход: объект заказа с суммой и признаком премиума. Act - ровно один вызов кода: const price = calculateDelivery(order). Assert сверяет наблюдаемый результат: expect(price).toBe(300). Три фазы не украшение, а разметка смысла: сразу видно, что подано на вход, какое действие проверяется и какой исход считается верным.
Один тест описывает одно поведение, а имя читается как строка спецификации. 'Делает доставку бесплатной от 5 000 рублей' - правило, понятное и не-программисту; оно переживёт любой рефакторинг внутренностей. При этом одно поведение может требовать нескольких проверок: если функция вернёт объект с ценой и сроком, сверить оба поля в одном тесте честнее двух отдельных. Правило не 'один expect на тест', а 'один исход на тест'.
Дальше выбирают случаи, и хороший набор покрывает три вида. Обычный - типичный вход посреди диапазона: заказ на 1 000 рублей, доставка 300. Граничный - точки, где поведение переключается: ровно 5 000, где доставка бесплатна, и 4 999 - на рубль меньше; на границах прячется большинство ошибок вроде 'больше' вместо 'больше или равно'. Невалидный - вход, который нельзя принимать молча: отрицательная сумма или null.
Когда случаев несколько, а логика проверки одна, их не дублируют руками - используют test.each, таблицу входов и ожиданий. Каждая строка становится отдельным тестом с именем из данных, так что в отчёте видно, какая именно пара упала. Это продолжение мысли о чистой функции: её поведение и есть таблица.
Не всякий код заслуживает теста, и знать это так же важно, как уметь писать проверки. Тривиальный геттер, тонкая обёртка над библиотекой, тест, повторяющий реализацию слово в слово, - доверия не добавляют, зато добавляют вес при каждом рефакторинге. Не стоит тестировать чужую библиотеку - это не ваш баг - и приватные детали: тест на приватный метод краснеет от переименования, хотя поведение не изменилось. Если тест не может упасть по осмысленной причине, его не пишут.
Цена продуманного выбора - несколько минут на вопрос 'какое поведение и какие случаи' перед первым expect. Выгода - набор, ловящий настоящие регрессии и не сыплющийся от косметических правок; те же принципы позже перенесутся на компонент - у кнопки корзины наблюдаемое поведение это надпись и реакция на клик, а не внутренний state. Типичный провал: сотня тестов на геттеры даёт красивое покрытие и ноль пойманных багов, а непроверенная граница '5 000 или 4 999' роняет продакшн.
describe('calculateDelivery', () => {
test('берёт 300 рублей за обычный заказ', () => {
const order = { total: 1_000, premium: false } // Подготовка
const price = calculateDelivery(order) // Действие
expect(price).toBe(300) // Проверка
})
})test.each([
{ total: 1_000, expected: 300 }, // обычный случай
{ total: 5_000, expected: 0 }, // граница: бесплатно от 5 000
{ total: 4_999, expected: 300 }, // граница: на рубль меньше
])('сумма $total -> доставка $expected', ({ total, expected }) => {
expect(calculateDelivery({ total, premium: false })).toBe(expected)
})