Тестирование граничных случаев

При работе с датами в JavaScript через Day.js критические ошибки чаще всего проявляются не в типовых сценариях, а при обработке крайних значений: минимальных и максимальных временных меток, переходов между месяцами, високосных лет, временных зон и некорректного входного формата. Day.js предоставляет компактный API, но сохраняет поведение JavaScript Date, что важно учитывать при тестировании.

Минимальные и максимальные значения времени

В основе Day.js лежит объект Date, который опирается на диапазон времени, ограниченный реализацией ECMAScript: примерно от -8640000000000000 до 8640000000000000 миллисекунд относительно Unix Epoch.

Типичные граничные сценарии:

import dayjs from 'dayjs';

const minDate = dayjs(-8640000000000000);
const maxDate = dayjs(8640000000000000);

console.log(minDate.isValid());
console.log(maxDate.isValid());

Проблемы возникают при:

  • арифметике дат, выходящей за пределы диапазона;
  • преобразовании больших timestamp из внешних систем;
  • сериализации дат в JSON и обратно.

Важно учитывать, что выход за пределы диапазона не всегда вызывает исключение — часто возвращается Invalid Date.


Нулевая точка и отрицательные timestamp

Unix Epoch (1970-01-01T00:00:00Z) является ключевой точкой для тестирования.

dayjs(0);               // Epoch
dayjs(-1);              // 1 миллисекунда до эпохи
dayjs(-1000 * 60 * 60); // 1 час до эпохи

Граничные дефекты проявляются при:

  • округлении отрицательных значений;
  • конвертации в локальное время;
  • использовании startOf('day') для дат до эпохи.

Особенность: Day.js корректно работает с отрицательными timestamp, но поведение может отличаться в старых окружениях Node.js.


Високосные годы и февральские границы

Один из наиболее чувствительных участков — 29 февраля.

dayjs('2024-02-29').isValid(); // true
dayjs('2023-02-29').isValid(); // false

Проблемные кейсы:

  • переход с 29 февраля на следующий год;
  • инкремент года через .add(1, 'year');
  • нормализация несуществующих дат.
dayjs('2024-02-29').add(1, 'year').format();
// часто приводит к 2025-03-01

Это связано не с Day.js, а с логикой JavaScript Date, который «перепрыгивает» на следующий корректный день.


Концы месяцев и переносы дат

Разная длина месяцев (28–31 день) создаёт неоднозначные сценарии.

dayjs('2023-01-31').add(1, 'month').format();
// ожидаемое: 2023-02-28 или 2023-03-03 (зависит от поведения)

Граничные случаи:

  • добавление месяцев к 29, 30, 31 числам;
  • использование endOf('month');
  • цепочки операций add → subtract → endOf.

Корректное тестирование должно фиксировать ожидаемую стратегию нормализации даты.


Переходы времени и часовые пояса

При подключении utc и timezone плагинов появляются дополнительные точки нестабильности.

import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';

dayjs.extend(utc);
dayjs.extend(timezone);

Ключевые граничные ситуации:

  • переход между UTC и локальным временем;
  • разница в смещении при DST (daylight saving time);
  • неоднозначные локальные времена (например, 01:30 может существовать дважды или не существовать).
dayjs.tz('2024-10-27 02:30', 'Europe/Berlin');

В момент перехода на зимнее время один и тот же локальный час может быть смещён или дублирован.


Летнее время (DST)

DST создаёт логические разрывы в последовательности времени.

Типовые проблемы:

  • несуществующие часы (spring forward);
  • повторяющиеся часы (fall back);
  • некорректный diff между соседними временными точками.
const a = dayjs.tz('2024-03-31 01:59', 'Europe/Berlin');
const b = dayjs.tz('2024-03-31 03:00', 'Europe/Berlin');

b.diff(a, 'minute');

Результат может отличаться от ожидаемого арифметического значения.


Некорректные и частично валидные строки

Day.js поддерживает ISO 8601 и ряд нестрогих форматов, но это источник неоднозначностей.

dayjs('2024-13-01'); // invalid month
dayjs('2024-00-10'); // невалидный месяц
dayjs('2024-02-30'); // несуществующая дата

Особое внимание требуется к «почти валидным» строкам:

  • 2024-02-31 → нормализуется в март;
  • 2024/02/31 → зависит от парсера окружения;
  • 2024-2-5 → может интерпретироваться неоднозначно.

Граничное тестирование должно включать:

  • пустые строки;
  • null, undefined;
  • случайные строки;
  • числовые строки.

Unix timestamp в секундах и миллисекундах

Частая ошибка — смешивание единиц измерения.

dayjs(1710000000);     // интерпретация как миллисекунды
dayjs.unix(1710000000); // интерпретация как секунды

Граничные случаи:

  • очень маленькие значения (например, 1, 10);
  • значения около 1e9 (секунды);
  • значения около 1e12 (миллисекунды).

Неверная интерпретация приводит к сдвигам на десятилетия.


Арифметика дат и накопление ошибок

Day.js является иммутабельным, но цепочки операций могут приводить к накоплению логических ошибок.

const d = dayjs('2024-01-31')
  .add(1, 'month')
  .add(1, 'month')
  .subtract(1, 'day');

Граничные сценарии:

  • многократные .add('month');
  • комбинации startOf и endOf;
  • чередование UTC и локального времени.

Каждый шаг может нормализовать дату по-своему.


Округление и разницы дат

Метод diff зависит от единицы измерения и округления.

dayjs('2024-01-01').diff('2023-12-31', 'day');
dayjs('2024-01-01').diff('2023-12-31', 'hour');

Проблемные зоны:

  • дробные значения (час → день);
  • отрицательные разницы;
  • потеря точности при миллисекундах.

Граничное тестирование должно учитывать:

  • разницу в 1 миллисекунду;
  • разницу ровно в 24 часа;
  • переход через полночь.

Форматирование крайних значений

Метод format может скрывать внутренние проблемы данных.

dayjs('invalid').format();
dayjs(0).format('YYYY-MM-DD HH:mm:ss');

Граничные случаи:

  • invalid date → строковое представление;
  • экстремальные даты → переполнение формата;
  • локализация форматов через locale.

Особенно важно проверять:

  • стабильность формата при разных локалях;
  • корректность нулевых значений;
  • поведение при отсутствии данных.

Поведение при клонировании и мутациях

Хотя Day.js иммутабелен, тестирование часто выявляет логические ошибки в ожиданиях разработчиков.

const a = dayjs('2024-01-01');
const b = a.add(1, 'day');

console.log(a.format());
console.log(b.format());

Граничные ситуации:

  • ожидание мутации вместо создания нового объекта;
  • повторное использование исходной ссылки;
  • цепочки вызовов в циклах.

Стратегии тестирования граничных случаев

Эффективное тестирование Day.js-кода обычно строится на комбинации подходов:

1. Табличные тесты

Использование фиксированных наборов дат:

[
  ['2024-02-29', true],
  ['2023-02-29', false],
  ['2024-13-01', false]
]

2. Проверка инвариантов

  • add(x).subtract(x) должно возвращать исходное значение (с оговорками по месяцам);
  • isValid() не должен зависеть от форматирования;
  • startOf('day') всегда ≤ исходной даты.

3. Property-based тестирование

Генерация случайных дат:

  • timestamp ± большие диапазоны;
  • случайные строки;
  • случайные комбинации операций.

4. Тестирование переходов времени

  • конец месяца;
  • високосные годы;
  • DST переходы.

Особенности поведения при цепочках плагинов

Day.js расширяется через плагины, и порядок их подключения может влиять на результат.

Граничные сценарии:

  • utc + timezone + relativeTime;
  • форматирование после преобразования зоны;
  • повторное применение расширений.

Стабильность в разных окружениях

Поведение Day.js зависит от среды выполнения:

  • Node.js версии;
  • браузерные реализации Date;
  • ICU data (для локалей);
  • polyfills.

Граничные ошибки часто проявляются при:

  • переносе между средами;
  • различиях в таймзонах ОС;
  • отсутствии полной ICU поддержки.

Обработка невалидных входных данных

Day.js не выбрасывает исключения при ошибках парсинга, а возвращает объект с флагом невалидности.

const d = dayjs('not-a-date');

d.isValid(); // false

Граничные сценарии:

  • цепочки методов на invalid объекте;
  • форматирование invalid даты;
  • сравнение invalid значений.

Поведение может быть неожиданным, если не проверяется isValid() на раннем этапе.