Чувствительное равенство.
Тест проверяет поведение, сравнивая строковое представление объекта (toString(), JSON.stringify(), отрендеренный HTML) с ожидаемым строковым литералом, тем самым привязываясь к несущественным деталям форматирования вместо действительно важных значений.
##Signs and Symptoms
Чувствительное равенство узнаётся, когда фактическая сторона проверки сериализует объект в текст, а ожидаемая сторона — строковый литерал, в котором множество полей слеплены вместе разделителями, кавычками, скобками и пробелами.
Характерные признаки:
- Фактическое значение получено из
.toString(),String(x), шаблонного литерала,JSON.stringify(...),wrapper.html(),el.outerHTMLили снапшота сериализованного кома. - Ожидаемое значение — длинная строка-литерал (нередко вставленная из предыдущего упавшего прогона), и с первого взгляда не понять, какая её часть и есть проверяемое.
- Тест ломается от изменений, не влияющих на поведение: переставленные ключи, добавленные/убранные пробелы, точность чисел, формат даты, локаль, часовой пояс или порядок обхода
Map/Set/объекта.
// Сравнение сериализованной формы со строковым литералом
expect(JSON.stringify(user)).toBe('{"name":"Bob","age":30}');
// Проверка вывода toString()
expect(order.toString()).toBe('Order[id=7, total=42.00, items=2]');
// Сравнение полного отрендеренного HTML с литералом
expect(wrapper.html()).toBe('<div class="card"><h2>Bob</h2><span>30</span></div>');
// Сравнение строк, чувствительное к локали/часовому поясу
expect(d.toString()).toBe('Mon Jun 15 2026 00:00:00 GMT+0000 (UTC)');
##Reasons for the Problem
Почему это происходит
- Это быстро и легко. Как отмечают ван Дёрсен и др. в Refactoring Test Code, быстро вычислить фактический результат, превратить его в строку и сравнить со строковым литералом, представляющим ожидаемое значение, — причём литерал нередко просто скопирован из упавшего прогона. Инструменты снапшотов делают это ещё соблазнительнее.
- У объекта не настроено осмысленное равенство (
equals/глубокий матчер), либо граф объекта велик, поэтому сбросить его в строку кажется проще, чем проверять поле за полем.
Чем это вредит
- Хрупкость / ложные падения. Строка несёт массу несущественных деталей — запятые, кавычки, пробелы, порядок ключей, точность чисел с плавающей точкой, локаль, часовой пояс, порядок обхода коллекций. Стоит измениться формату
toString()/сериализации, как падать начинают никак не связанные тесты, хотя поведение корректно. Это классическая причина хрупкого теста. - Сопровождаемость. Единственное изменение форматирования или библиотеки вынуждает править множество тестов, а гигантский ожидаемый литерал утомительно обновлять и легко испортить незаметной ошибкой.
- Читаемость / скрытое намерение. Ожидаемое значение в виде стены текста скрывает, что именно проверяется; ревьюер не видит того единственного поля, которое важно тесту.
- Ложная уверенность. Строковый ком может пройти по неверным причинам (два разных состояния сериализуются в один и тот же текст) и подталкивает разработчиков перезаписывать литерал/снапшот при падении, не проверяя, что новое значение действительно корректно.
- Флакование. Чувствительные к локали, часовому поясу и порядку строки ставят исход в зависимость от окружения, в котором выполняется тест.
##Treatment
Проверяйте данные, а не их отображение.
- Сравнивайте структурированные значения глубоким равенством вместо строкового (
toEqual/toStrictEqual), которое для объектов не зависит от порядка и пробелов. - Проверяйте только значимые атрибуты частичными матчерами (
toMatchObject,expect.objectContaining), чтобы изменение форматирования в другом месте не могло сломать тест. - Разбирайте, а не сопоставляйте строки. Получив сериализованную полезную нагрузку (например, JSON-тело API), разберите её обратно в структуру и сравнивайте структуры.
- Введите метод равенства/сравнения у объекта (рефакторинг, который рекомендуют ван Дёрсен и др.) и проверяйте равенство объектов, а не равенство
toString(). - Если без сравнения сериализованного вывода не обойтись, сначала канонизируйте — отсортируйте ключи, зафиксируйте локаль/часовой пояс, задайте точность чисел — и предпочтите проверенный снапшот вручную вставленному встроенному литералу.
- Для тестов DOM/компонентов делайте семантические запросы (Testing Library
getByRole,toHaveTextContent) вместо сравнения всего HTML.
// До — чувствительное равенство
expect(JSON.stringify(user)).toBe('{"name":"Bob","age":30}');
// После — структурное равенство (не зависит от порядка ключей / пробелов)
expect(user).toEqual({ name: 'Bob', age: 30 });
// После — проверяем только нужное поле
expect(user).toMatchObject({ age: 30 });
// До — строковое сравнение полезной нагрузки API
expect(res.text).toBe('{"id":7,"total":42}');
// После — сравниваем разобранные структуры
expect(JSON.parse(res.text)).toEqual({ id: 7, total: 42 });