ConstructiCat Logo
CodeBust.
Browse section ▾

Чувствительное равенство.

Тест проверяет поведение, сравнивая строковое представление объекта (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

Проверяйте данные, а не их отображение.

  1. Сравнивайте структурированные значения глубоким равенством вместо строкового (toEqual/toStrictEqual), которое для объектов не зависит от порядка и пробелов.
  2. Проверяйте только значимые атрибуты частичными матчерами (toMatchObject, expect.objectContaining), чтобы изменение форматирования в другом месте не могло сломать тест.
  3. Разбирайте, а не сопоставляйте строки. Получив сериализованную полезную нагрузку (например, JSON-тело API), разберите её обратно в структуру и сравнивайте структуры.
  4. Введите метод равенства/сравнения у объекта (рефакторинг, который рекомендуют ван Дёрсен и др.) и проверяйте равенство объектов, а не равенство toString().
  5. Если без сравнения сериализованного вывода не обойтись, сначала канонизируйте — отсортируйте ключи, зафиксируйте локаль/часовой пояс, задайте точность чисел — и предпочтите проверенный снапшот вручную вставленному встроенному литералу.
  6. Для тестов 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 });