Тест - это короткое утверждение о поведении кода, записанное так, что машина может его проверить. В Vitest минимальный тест состоит из двух вещей: функции test, которая принимает имя и колбэк, и внутри - выражения expect(значение).матчер(ожидание). Имя объясняет, что проверяется; колбэк выполняет проверяемое действие; матчер сравнивает результат с ожиданием и решает, зелёный тест или красный. Никакой магии здесь нет, и именно поэтому на этом стоит всё остальное.
Связанные тесты группируют в describe - обычно по сущности или сценарию. Он не обязателен, но даёт вложенное имя в отчёте и место для общего setup. describe('calculateDelivery', ...) читается как заголовок раздела, а вложенные test - как его пункты. Пока тестов немного, можно обойтись и плоским списком; describe появляется, когда у группы есть общий контекст.
Запускается всё командой vitest: без аргументов это watch в интерактивном терминале, который перезапускает затронутые тесты и даёт фильтры прямо в консоли, а vitest run - один проход для CI. При первом запуске Vitest находит файлы по маске, прогоняет их и печатает сводку: сколько прошло, сколько упало и за какое время. Watch удобен в разработке, run - в пайплайне. Отдельные тесты фильтруют по имени флагом -t, а по пути - просто указав его в команде.
Главный навык новичка - читать падение, а не бояться его. Vitest показывает, что он ожидал (Expected) и что получил (Received), и указывает на конкретную строку с матчером. Если calculateDelivery вернул 250 вместо 300, в отчёте видно и то, и другое, и место, где сравнение не сошлось. Красный тест - это не поломка процесса, а точное сообщение о том, что именно разошлось с ожиданием.
Матчер выбирают под тип значения. toBe сравнивает примитивы и ссылки через Object.is и годится для чисел, строк и булевых; для объектов и массивов нужен toEqual, который сравнивает по значению рекурсивно. Точные матчеры - отдельная глава, но правило простое уже сейчас: чем конкретнее матчер, тем понятнее сообщение об ошибке.
Профессиональная привычка - красно-зелёный цикл. Сначала пишут падающий тест на желаемое поведение и убеждаются, что он действительно красный и падает по делу. Потом добавляют минимальный код, пока он не станет зелёным. Порядок важен: тест, который никогда не видели красным, может проходить из-за опечатки в матчере или лишнего условия и на самом деле ничего не проверять.
Имя теста стоит писать как предложение об условии и наблюдаемом исходе: 'берёт 300 рублей за обычную доставку', а не 'works' или 'test1'. Тогда список тестов читается как спецификация поведения, а красное имя в CI сразу говорит, что именно сломалось, - не приходится лезть в код, чтобы понять, о чём был тест.
Типичный провал новичка - тест без утверждения: колбэк вызывает функцию, но ничего не сравнивает. Такой тест всегда зелёный и не проверяет ничего, кроме того, что вызов не бросил исключение. Рядом с ним стоит второй капкан - ассерт, который никогда не падал: если убрать сам код и тест всё равно зелёный, он не защищает поведение, а лишь создаёт иллюзию покрытия.
FAIL src/delivery.test.ts > берёт 300 рублей за обычную доставку
expect(received).toBe(expected) // Object.is equality
Expected: 300
Received: 250
6| expect(calculateDelivery(order)).toBe(300)
| ^import { expect, test } from 'vitest'
import { calculateDelivery } from './delivery.js'
test('берёт 300 рублей за обычную доставку', () => {
const order = { total: 1_000, premium: false }
expect(calculateDelivery(order)).toBe(300)
})