Мониторинг мониторинга - звучит как масло масляное, но именно этим мы постоянно занимаемся в PingZen. Ведь наш сервис должен быть надёжнее, чем объекты, которые он отслеживает. Если PingZen упадёт, кто сообщит о падении вашего сайта?
Сегодня я залезу под капот тестирования PingZen и расскажу, как мы дошли до 10 900 автоматических тестов (5100 на бэкенде и 5812 на фронтенде). Все цифры и технологии - строго из нашего репозитория . Без хайпа, только факты, код и архитектурные решения.А статью подробнее про Pingzen можете посмотреть здесь
Почему тесты - это не роскошь, а necessity для мониторинга
Когда я начинал проект, у меня была иллюзия: «Мониторинг - это просто проверять HTTP-статус, тут ошибиться сложно». Реальность оказалась иной:
22 протокола - от HTTP до Minecraft handshake и DNS over TCP. Каждый имеет свои тонкости (например, UDP-пакеты могут теряться, и это не всегда означает сбой сервера).
Сложная логика детекции сбоев - нужно отличать временные сетевые глюки от реального падения. У нас есть настройки
confirmation_threshold(сколько неудач подряд нужно для статуса DOWN) иrecovery_threshold(сколько успешных для восстановления). Если в этой логике ошибка, пользователи будут получать ложные тревоги или, что ещё хуже, пропускать реальные падения.Метрики и временные ряды - мы храним историю проверок в VictoriaMetrics. Неправильная запись или агрегация метрик приведёт к искажённым графикам и отчётам.
Уведомления - алерты должны уходить в Telegram, Slack, Email, Discord, Webhooks без дубликатов и с гарантией доставки. Одно неверное условие - и ночью проснётся вся команда.
Масштабирование - сейчас у нас ~191 монитор (активных), но мы готовимся к росту. Тесты должны гарантировать, что при увеличении нагрузки ничего не развалится.
Без автоматических тестов поддерживать такой сервис было бы невозможно. Каждое изменение в коде могло бы что-то сломать, и мы бы узнавали об этом от пользователей. Тесты - это наша страховка и документация одновременно.
Пирамида тестирования в PingZen
Мы следуем классической пирамиде тестирования (по Майклу Койнусу), но адаптировали её под свой стек:
Юнит-тесты (нижний уровень) - изолированно проверяют функции и классы. Их больше всего (около 60% всех тестов).
Интеграционные тесты - проверяют взаимодействие модулей: работа с БД, Redis, VictoriaMetrics.
API-тесты - тестируют эндпоинты FastAPI.
Компонентные тесты фронтенда - изолированно проверяют React-компоненты.
E2E-тесты (верхушка пирамиды) - проверяют критические пользовательские сценарии от начала до конца. Их меньше всего (около 500), но они самые важные.
Такая структура даёт оптимальное соотношение скорости выполнения и уверенности в качестве. Юнит-тесты выполняются за секунды, E2E - за минуты, но каждый уровень ловит свои классы ошибок.
Бэкенд: 5100 тестов на Python 3.13 + FastAPI
Весь бэкенд написан на современном Python с активным использованием asyncio. Для тестирования мы выбрали pytest - он стал стандартом индустрии благодаря своей мощи и экосистеме. В репозитории вы найдёте папку backend/tests с полным покрытием. Тесты организованы по модулям: test_monitors, test_checks, test_notifications и т.д.
Юнит-тесты: база, на которой всё держится
Юнит-тесты изолируют одну единицу кода - функцию или метод класса. Мы используем моки (unittest.mock или pytest-mock), чтобы отрезать зависимости от внешних сервисов. Это позволяет тестам быть быстрыми и детерминированными.
Пример: тестирование парсера для UDP-мониторинга Minecraft
В модуле pinger есть функция, которая формирует «ping-пакет» для Minecraft сервера и парсит ответ. Такие функции критичны: если парсер сломается, пользователи перестанут получать корректные статусы своих игровых серверов.
# mc_parser.py async def parse_minecraft_response(raw_packet: bytes) -> dict: # ... парсинг пакета по протоколу (см. репозиторий) return {"players_online": players, "version": version, "motd": motd}
Тест для неё выглядит так:
import pytest from mc_parser import parse_minecraft_response def test_parse_valid_mc_response(): # Подготовка: байтовый пакет, записанный с реального сервера raw = b'\x00\xa4\x00...' # реальный дамп из репозитория result = parse_minecraft_response(raw) assert result["players_online"] == 42 assert "Paper 1.20" in result["version"] assert "My Server" in result["motd"]
Обратите внимание: мы не ходим в сеть, не поднимаем настоящий Minecraft сервер. Мы используем заранее сохранённый дамп ответа. Это делает тест быстрым и воспроизводимым. Если кто-то случайно изменит парсер, тест упадёт, и мы сразу увидим проблему.
Интеграционные тесты: проверяем связку компонентов
Интеграционные тесты поднимают реальные зависимости: PostgreSQL (в тестовом контейнере), Redis и VictoriaMetrics. Они проверяют, что наш код правильно взаимодействует с этими сервисами.
Пример: тестирование логики подтверждения сбоя
Логика confirmation_threshold и recovery_threshold реализована в ядре pinger и использует Redis для хранения счётчиков неудач. Нам нужно убедиться, что пороги работают именно так, как задумано.
# test_failure_detection.py import pytest from unittest.mock import patch @pytest.mark.asyncio async def test_confirmation_threshold_works(db_session, redis_client): # Создаём монитор с порогом 3 monitor = await create_test_monitor(db_session, confirmation=3, recovery=2) # Эмулируем 2 неудачные проверки for _ in range(2): await run_check(monitor.id, success=False) # Проверяем, что статус ещё "UP" status = await get_monitor_status(monitor.id) assert status == "UP" # Третья неудача await run_check(monitor.id, success=False) status = await get_monitor_status(monitor.id) assert status == "DOWN" # Порог сработал # Теперь две успешные проверки await run_check(monitor.id, success=True) await run_check(monitor.id, success=True) status = await get_monitor_status(monitor.id) assert status == "UP" # Восстановился
Этот тест поднимает настоящие БД и Redis, но использует изолированные таблицы (транзакции откатываются). Он гарантирует, что наша бизнес-логика не сломается при изменениях. Без таких тестов мы бы не решились менять что-то в этом механизме.
Тестирование записи метрик в VictoriaMetrics
Метрики по каждому монитору (время ответа, статус, код ошибки) пишутся в VictoriaMetrics. Мы тестируем этот процесс с помощью специального тестового инстанса VictoriaMetrics, поднятого в контейнере.
# test_metrics_writer.py async def test_metrics_are_written(victoria_client, test_monitor): # Запускаем проверку и записываем метрику await record_metric(test_monitor.id, response_time=0.42, status="UP") # Читаем последние метрики через VictoriaMetrics API metrics = await victoria_client.query(f'response_time{{monitor="{test_monitor.id}"}}') assert len(metrics) > 0 assert metrics[0]["value"] == 0.42
Так мы уверены, что дашборды и графики не врут. Кстати, для локальной разработки мы используем victoriametrics в docker-compose, что позволяет легко писать и запускать такие тесты.
Тесты API: проверяем эндпоинты
FastAPI предоставляет удобный TestClient на основе Starlette. Мы написали более 500 тестов API, покрывающих все эндпоинты: создание, обновление, удаление мониторов, получение статистики, управление пользователями и т.д.
# test_api_monitors.py from fastapi.testclient import TestClient def test_create_monitor(client: TestClient, auth_headers): response = client.post( "/api/monitors", json={ "name": "My Site", "type": "http", "target": "https://example.com", "interval": 60 }, headers=auth_headers ) assert response.status_code == 201 data = response.json() assert data["name"] == "My Site" assert "id" in data
Эти тесты не только проверяют корректность ответов, но и валидируют схемы данных (мы используем Pydantic, и тесты помогают убедиться, что сериализация работает правильно). Также они проверяют обработку ошибок: например, что будет, если передать неверный тип протокола.
Тестирование асинхронных воркеров
Самые сложные тесты - для фоновых задач (воркеров), которые запускают проверки и отправляют уведомления. Эти воркеры работают в отдельных процессах и общаются через Redis. Мы используем паттерн «fake-зависимости»: вместо реального SMTP-сервера подставляем тестовый, который сохраняет отправленные письма в память.
# test_notification_worker.py async def test_telegram_notification_sent(): # Подготовка: создаём монитор с алертом в Telegram monitor = await create_monitor_with_telegram() # Эмулируем падение монитора await trigger_failure(monitor.id) # Запускаем воркер уведомлений (в тестовом режиме) await notification_worker.run_once() # Проверяем, что в тестовый Telegram-аккаунт пришло сообщение sent = await get_fake_telegram_messages() assert any("DOWN" in msg.text for msg in sent)
Этот тест проверяет всю цепочку: монитор падает → воркер уведомлений забирает событие → отправляет сообщение в Telegram. Если бы мы не тестировали это, могли бы пропустить ситуацию, когда сообщения не уходят из-за ошибки в форматировании текста.
Отдельный кластер тестов для HCP-сервера
Наш HCP-сервер (тот самый, с 34 инструментами) тоже покрыт тестами. Мы проверяем, что каждая команда (ping, traceroute, dig, openssl и даже playwright) возвращает корректный структурированный вывод.
# test_hcp_commands.py async def test_hcp_ping_parsing(hcp_client): result = await hcp_client.execute("ping", ["-c", "3", "8.8.8.8"]) assert result["parsed"]["loss_percent"] == 0 assert result["parsed"]["rtt"]["avg"] > 0
Фронтенд: 5812 тестов на React и TypeScript
На фронтенде у нас React + TypeScript. Папка frontend/src/__tests__ содержит тысячи тестов. Мы используем современный стек: Jest, React Testing Library, Playwright для E2E. Фронтенд-тестирование имеет свою специфику: нужно проверять не только логику, но и отображение, доступность, взаимодействие с пользователем.
Компонентные тесты (Jest + React Testing Library)
Мы тестируем каждый UI-компонент изолированно. Это позволяет быстро выявлять ошибки в вёрстке, пропсах и обработчиках событий. Особое внимание уделяем доступности (accessibility) - у нас реализовано более 20 критериев WCAG 3/4, и каждый проверяется автоматически с помощью jest-axe.
// StatusBadge.test.tsx import { render, screen } from '@testing-library/react'; import { axe, toHaveNoViolations } from 'jest-axe'; import { StatusBadge } from './StatusBadge'; expect.extend(toHaveNoViolations); test('displays "UP" status with green color', () => { render(<StatusBadge status="UP" />); const badge = screen.getByText('UP'); expect(badge).toHaveClass('bg-green-500'); }); test('should have no accessibility violations', async () => { const { container } = render(<StatusBadge status="DOWN" />); const results = await axe(container); expect(results).toHaveNoViolations(); });
Первый тест проверяет визуальное оформление (через CSS-класс), второй - что компонент доступен для людей с ограниченными возможностями (например, что у него правильный контраст и ARIA-атрибуты). Мы запускаем такие тесты для всех компонентов, и это помогает держать планку доступности.
Хуки и утилиты
Сложная логика, например форматирование времени аптайма или фильтрация мониторов, вынесена в кастомные хуки и утилиты. Их мы тоже тестируем изолированно.
// useMonitorFilter.test.ts import { renderHook, act } from '@testing-library/react'; import { useMonitorFilter } from './useMonitorFilter'; test('filters monitors by name', () => { const monitors = [{ name: 'Alpha' }, { name: 'Beta' }]; const { result } = renderHook(() => useMonitorFilter(monitors)); act(() => { result.current.setFilter('alp'); }); expect(result.current.filteredMonitors).toEqual([{ name: 'Alpha' }]); });
Здесь мы проверяем, что хук правильно обновляет состояние при изменении фильтра. Это особенно важно, потому что такие хуки используются в разных частях приложения, и ошибка в них привела бы к некорректной работе поиска или сортировки.
E2E-тесты с Playwright
Мы не ограничиваемся юнитами. Около 500 E2E-тестов на Playwright проверяют критические пользовательские сценарии: логин (через Telegram, Google, Yandex, Discord - у нас 5 методов авторизации), создание монитора, просмотр статус-страницы, настройку уведомлений.
// create-monitor.spec.ts import { test, expect } from '@playwright/test'; test('user can create an HTTP monitor', async ({ page }) => { await page.goto('/dashboard'); await page.click('text=Create Monitor'); await page.fill('[name=name]', 'Habr Test'); await page.fill('[name=target]', 'https://habr.com'); await page.selectOption('[name=type]', 'http'); await page.click('button:has-text("Create")'); await expect(page.locator('text=Monitor created')).toBeVisible(); await expect(page.locator('text=Habr Test')).toBeVisible(); });
Playwright запускается в CI на трёх браузерах (Chromium, Firefox, WebKit), что гарантирует кроссплатформенность. Отдельные тесты проверяют локализацию (у нас полная поддержка EN/RU). Например, мы проверяем, что при смене языка интерфейса все надписи корректно переводятся.
Инфраструктура тестирования: GitLab CI
Все тесты автоматически запускаются в GitLab CI при каждом пуше в репозиторий. Наш pipeline настроен так:
Линтеры (ruff, eslint, prettier) - быстрая проверка стиля кода.
Юнит-тесты бэкенда - запуск с pytest, параллельно на 4 потока.
Юнит-тесты фронтенда - Jest с --maxWorkers=2.
Интеграционные тесты - требуют PostgreSQL, Redis и VictoriaMetrics, поднимаются в контейнерах (см.
docker-compose.test.ymlв репозитории).E2E-тесты - самый долгий этап, но он критичен.
Если любой этап падает, merge request блокируется. У нас есть правило: «ни одного мержа с падающими тестами». Это дисциплинирует и заставляет писать тесты на новые фичи сразу, а не откладывать на потом.
Кстати, мы используем GitLab CI cache для ускорения: кэшируем зависимости Python (pip) и Node.js (node_modules), а также отчёты о покрытии.
Покрытие кода: насколько это много?
Мы измеряем покрытие с помощью pytest-cov (бэкенд) и Jest --coverage (фронтенд). Сейчас показатели такие:
Бэкенд: 87% строк покрыто тестами.
Фронтенд: 82% (JSX/TSX сложнее покрывать, но мы стараемся).
10 900 тестов - это не самоцель, а следствие подхода «тестируй всё, что может сломаться». Иногда один тест покрывает сотни строк, иногда на одну сложную функцию приходится 10 тестов. Мы не гонимся за 100% покрытием, потому что это часто приводит к тестированию тривиальных геттеров и сеттеров, но стараемся держать планку выше 80%.
Как тесты помогают пользователям PingZen
Никаких регрессий. Мы можем смело обновлять версии Python, FastAPI, React, не боясь, что что-то отвалится. Например, недавно мы обновили FastAPI с 0.68 до 0.115 - тесты помогли найти пару мест, где изменилось поведение валидации.
Новые фичи без страха. Добавить поддержку нового протокола (например, MQTT) - значит сначала написать тесты, потом код. Так мы уверены, что новая фича не сломает старую.
Мгновенная обратная связь разработчикам. Если тесты зелёные, можно деплоить в прод. Мы деплоим несколько раз в день, и это безопасно благодаря тестам.
Документация в действии. Тесты - это лучшая документация: они показывают, как должен работать код. Новый разработчик может заглянуть в тесты и понять ожидаемое поведение функции.
Заключение: приходите убедиться сами
10 900 тестов - это не просто цифра для галочки. Это наша гарантия, что PingZen будет работать стабильно и предсказуемо. Мы вложили тысячи часов в написание этих тестов, чтобы вы могли спать спокойно, зная, что ваш мониторинг не подведёт.
Хотите проверить надёжность на практике? Зарегистрируйтесь на PingZen.dev (это бесплатно для небольших проектов - 55 мониторов, 60-секундный интервал, 6 каналов уведомлений) и создайте свой первый монитор за минуту. А если найдёте баг - напишите нам, мы добавим тест, который его предотвратит 😉
