Интеграционные тесты

Интеграционные тесты проверяют совместную работу нескольких частей приложения: компонентов с реальным DOM, хуков с библиотекой, взаимодействие рендеринга с таймерами. В отличие от unit тестов, они не изолируют timeago.js — библиотека работает по-настоящему.


Принцип интеграционных тестов

Unit тест: formatDate(null) возвращает ''. Интеграционный тест: компонент <TimeAgo date={null} /> рендерит заглушку, а не падает.

Интеграционный тест ближе к реальному использованию. Он проверяет, что все части работают вместе.


Настройка среды

// jest.config.js
module.exports = {
  testEnvironment:    'jsdom',
  setupFilesAfterFramework: ['@testing-library/jest-dom'],
};
npm install --save-dev @testing-library/react @testing-library/jest-dom

Тест компонента с реальным timeago.js

// src/components/TimeAgo.tsx
import { format } from 'timeago.js';

interface Props {
  date:    Date | string | number;
  locale?: string;
}

export function TimeAgo({ date, locale = 'ru' }: Props) {
  return <time dateTime={new Date(date as any).toISOString()}>{format(date, locale)}</time>;
}
// src/components/TimeAgo.test.tsx
import { render, screen } from '@testing-library/react';
import { TimeAgo } from './TimeAgo';

const FIXED = new Date('2025-06-01T12:00:00Z').getTime();

beforeEach(() => {
  jest.useFakeTimers();
  jest.setSystemTime(FIXED);
});

afterEach(() => jest.useRealTimers());

describe('TimeAgo component (integration)', () => {
  it('рендерит элемент time с корректным datetime', () => {
    const date = new Date(FIXED - 3600_000);
    render(<TimeAgo date={date} />);
    const el = screen.getByRole('time');
    expect(el).toBeInTheDocument();
    expect(el).toHaveAttribute('dateTime');
  });

  it('рендерит относительное время для 1 часа назад', () => {
    render(<TimeAgo date={new Date(FIXED - 3600_000)} />);
    expect(screen.getByText(/час/)).toBeInTheDocument();
  });

  it('рендерит с другой локалью', () => {
    render(<TimeAgo date={new Date(FIXED - 3600_000)} locale="en_US" />);
    expect(screen.getByText(/ago|hour/i)).toBeInTheDocument();
  });
});

Тест живого компонента с render/cancel

// src/components/LiveTimeAgo.tsx
import { useEffect, useRef } from 'react';
import { render, cancel } from 'timeago.js';

export function LiveTimeAgo({ date, locale = 'ru' }: { date: Date; locale?: string }) {
  const ref = useRef<HTMLTimeElement>(null);

  useEffect(() => {
    if (!ref.current) return;
    render(ref.current, locale);
    return () => { if (ref.current) cancel(ref.current); };
  }, [locale]);

  return <time ref={ref} dateTime={date.toISOString()} />;
}
// src/components/LiveTimeAgo.test.tsx
import { render as rtlRender, screen, waitFor } from '@testing-library/react';
import { LiveTimeAgo } from './LiveTimeAgo';

const FIXED = new Date('2025-06-01T12:00:00Z').getTime();

beforeEach(() => {
  jest.useFakeTimers();
  jest.setSystemTime(FIXED);
});

afterEach(() => jest.useRealTimers());

it('элемент time получает текст после монтирования', async () => {
  const date = new Date(FIXED - 60_000);
  rtlRender(<LiveTimeAgo date={date} />);

  await waitFor(() => {
    const el = document.querySelector('time');
    expect(el?.textContent).toBeTruthy();
  });
});

it('не выбрасывает при размонтировании', () => {
  const date = new Date(FIXED - 60_000);
  const { unmount } = rtlRender(<LiveTimeAgo date={date} />);
  expect(() => unmount()).not.toThrow();
});

Тест списка постов с timeago.js

// src/components/PostList.test.tsx
import { render, screen } from '@testing-library/react';
import { PostList } from './PostList';

const FIXED = new Date('2025-06-01T12:00:00Z').getTime();
jest.useFakeTimers();
jest.setSystemTime(FIXED);

const posts = [
  { id: 1, title: 'Первый пост',  createdAt: new Date(FIXED - 3600_000).toISOString() },
  { id: 2, title: 'Второй пост', createdAt: new Date(FIXED - 86400_000).toISOString() },
];

it('рендерит все посты с временем', () => {
  render(<PostList posts={posts} />);
  expect(screen.getByText('Первый пост')).toBeInTheDocument();
  expect(screen.getByText('Второй пост')).toBeInTheDocument();

  const timeElements = document.querySelectorAll('time');
  expect(timeElements).toHaveLength(2);
});

Тест смены локали

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { AppWithLocale } from './AppWithLocale';

const FIXED = new Date('2025-06-01T12:00:00Z').getTime();

it('обновляет время при смене локали', async () => {
  jest.useFakeTimers();
  jest.setSystemTime(FIXED);

  render(<AppWithLocale />);

  // По умолчанию — русская локаль
  expect(screen.getByText(/назад|только что/)).toBeInTheDocument();

  // Переключить на английский
  await userEvent.click(screen.getByRole('button', { name: /English/i }));

  expect(screen.getByText(/ago|just now/i)).toBeInTheDocument();

  jest.useRealTimers();
});

Тест регистрации кастомной локали

import { format, register } from 'timeago.js';

it('кастомная локаль отображает правильный текст', () => {
  register('test', (n, i) => {
    const forms: [string, string][] = [
      ['прямо сейчас', 'сейчас'],
      ['%s с назад', 'через %s с'],
    ];
    return forms[i] ?? ['давно', 'потом'];
  });

  jest.useFakeTimers();
  jest.setSystemTime(new Date('2025-06-01T12:00:00Z').getTime());

  const result = format(new Date('2025-06-01T11:59:59Z'), 'test');
  expect(result).toContain('с назад');

  jest.useRealTimers();
});

Тест с MutationObserver

it('MutationObserver отслеживает добавленные элементы', (done) => {
  jest.useFakeTimers();
  jest.setSystemTime(new Date('2025-06-01T12:00:00Z').getTime());

  const container = document.createElement('div');
  document.body.appendChild(container);

  const observer = new MutationObserver((mutations) => {
    mutations.forEach(mutation => {
      mutation.addedNodes.forEach(node => {
        if (node instanceof HTMLElement) {
          const times = node.querySelectorAll('[datetime]');
          if (times.length > 0) {
            expect(times.length).toBe(1);
            done();
          }
        }
      });
    });
  });

  observer.observe(container, { childList: true, subtree: true });

  const el = document.createElement('div');
  const time = document.createElement('time');
  time.setAttribute('datetime', '2025-06-01T11:00:00Z');
  el.appendChild(time);
  container.appendChild(el);

  observer.disconnect();
  jest.useRealTimers();
});

Что НЕ тестировать в интеграционных тестах

  • Внутренние таймеры timeago.js — это тестирование сторонней библиотеки.
  • Точные строки вывода в деталях (например, “2 часа назад” vs “2 ч. назад”) — форматирование может измениться.
  • Точное время обновления DOM — браузерные циклы нестабильны в тестовой среде.

Фокус: компонент монтируется без ошибок, отображает время, корректно очищает ресурсы.