Архитектура бэкенда
DI как шов для тестов: фейки, моки и граница, которая важна
Команда гордится набором тестов OrderService: 100% покрытия, каждая зависимость замокана, всё зелёное. Затем рефакторинг, ничего не меняющий в поведении — разбиение одного метода репозитория на два — делает сорок тестов красными. Тесты не проверяли, что заказы оформляются. Они проверяли, что repo.save вызван ровно один раз ровно с этими аргументами. Шов, который дал им DI, был реальным; они лишь нацелили его не туда.
Шов — это выигрыш
Всё в этом юните — внедрение через конструктор, корень композиции, абстракции вместо new — окупается здесь. Поскольку OrderService получает PaymentGateway, а не конструирует StripeClient, тест может передать замену. Эта замена — тестовый дубль, а точка внедрения — шов: стык, где продакшен-связывание меняется на тестовое. Нет шва — нет изолированного юнит-теста. Поэтому «тестируемо ли это?» и «внедрены ли зависимости?» — почти один вопрос.
Два дубля, противоположные цели
Слово «мок» используют вольно для любой замены, но различие — весь урок:
- Стаб / фейк заменяет зависимость и поставляет состояние. Фейковый
UserRepositoryна in-memoryMapведёт себя как реальный: сохранил пользователя — можешь прочитать обратно. Ваши проверки смотрят на результат — заказ оказался сохранён, возвращённая сумма верна. - Мок запрограммирован ожиданиями про вызовы. Он утверждает, что
payment.charge(amount)вызван один раз с этим аргументом. Ваши проверки смотрят на взаимодействие, не на исход.
Первый проверяет, что система сделала; второй — как она это сделала. Оба легитимны, но падают по-разному — и Hook это то, что бывает, когда мок используют для того, что должен был покрыть фейк.
Классицисты против лондонцев и почему это важно
Это раскол классицисты против мокистов («лондонская школа»). Мокисты мокают каждого коллаборатора и проверяют взаимодействия, так что каждый юнит тестируется в полной изоляции. Классицисты используют реальные объекты или фейки для коллабораторов, которыми владеют, и берегут моки для неудобных границ. Практическое следствие — связанность со структурой: полностью замоканный тест знает точную форму вызова своей зависимости, поэтому любой рефакторинг, сохраняющий поведение, но меняющий форму вызова, ломает тест. Это и есть баг сорока красных тестов. Тесты, проверяющие через состояние, переживают рефакторинги, ведь им важен лишь наблюдаемый результат.
Почему это работает
Почему тесты взаимодействий ломаются на рефакторингах, ничего не меняющих? Потому что ожидание мока есть утверждение о реализации. expect(repo.save).toHaveBeenCalledTimes(1) кодирует «продакшен-код вызывает save ровно один раз». Разбейте это на два save внутри транзакции — идентичное поведение, идентичное финальное состояние — и ожидание теперь ложно, хотя ничто наблюдаемое пользователем не изменилось. Тест мерил внутренние ходы кода, не его вывод. У тестов на состоянии этой проблемы нет: они спрашивают «после прогона заказ сохранён и сумма верна?», что инвариантно к любому рефакторингу, сохраняющему поведение. Моки не неправильны — они верный инструмент для проверки эффекта, который нельзя наблюдать через состояние, вроде «письмо отправлено» — но каждый мок это маленькая ставка, что именно эта форма вызова часть контракта.
Мокай на границе, фейкай то, чем владеешь
Дисциплина, избегающая чрезмерного мокания: мокай на краях системы, используй реальные объекты или фейки внутри неё. Код, которым владеешь и управляешь — доменные сервисы, твои репозитории — можно связать реальными экземплярами или in-memory-фейками, чтобы тесты упражняли настоящее взаимодействие. Мокать стоит границы, которыми не управляешь или которые не можешь позволить в тесте: платёжный шлюз, отправитель писем, часы, сторонний HTTP-вызов. Это ровно те зависимости, где хочешь утверждать «мы вызвали Stripe с этой суммой», ведь сам вызов и есть внешне видимый эффект. Шов ценнее всего именно на границе системы — где DI и важнее всего.
Чрезмерное мокание — запах дизайна
Когда юнит-тесту нужно десять моков, чтобы сконструировать субъект, проблема не в тесте — в дизайне. Класс, требующий десять коллабораторов, делает слишком много, и болезненный тест — гонец. Рефлекс сеньора — читать боль теста как обратную связь о связанности, а не как повод тянуться за бо́льшим мок-арсеналом. Трудно тестировать обычно значит трудно менять.
| Дубль | Поставляет | Вы проверяете | Ломается на |
|---|---|---|---|
| Фейк / стаб | Реалистичное состояние | Результат/исход | Только смена поведения |
| Мок | Записанные ожидания | Взаимодействие (вызовы) | Любая смена формы вызова |
| Реальный объект | Настоящее поведение | Результат/исход | Только смена поведения |
Сохраняющий поведение рефакторинг разбивает один `repo.save()` на два save внутри транзакции, и десятки тестов краснеют. Что это вскрывает о тех тестах?
Какую зависимость лучше всего заменить моком, проверяющим вызов, а не фейком, поставляющим состояние?
Юнит-тесту нужно десять моков лишь чтобы инстанцировать тестируемый класс. Каково сеньорское прочтение этой боли?
- 01Что такое шов для тестов и как DI его создаёт?
- 02В чём разница между фейком/стабом и моком и как они падают по-разному?
- 03Что за правило «мокай на границе, фейкай то, чем владеешь» и почему чрезмерное мокание сигналит проблему дизайна?
Шов, который создаёт внедрение зависимостей, — вся причина, по которой тестируемость и внедрение — один разговор: точка внедрения — это место, где продакшен-связывание уступает тестовому дублю. Но «дубль» прячет развилку. Фейк или стаб поставляет реалистичное состояние и даёт проверкам смотреть на исход, поэтому ломается лишь при настоящей смене поведения; мок записывает ожидания вызовов и проверяет взаимодействия, поэтому ломается на любом рефакторинге, меняющем форму вызова — причина того, что сохраняющее поведение изменение делает десятки тестов красными. Классицистская дисциплина держит тесты надёжными: мокай границы, которыми не владеешь (платёж, письма, часы, внешний HTTP), где сам вызов — видимый эффект, и связывай реальные объекты или фейки для кода, которым управляешь, проверяя через состояние. А когда тесту нужно десять моков лишь чтобы поставить субъект на ноги, боль — это дизайн говорит: слишком много коллабораторов, слишком много ответственности. С понятым швом последний урок поворачивает к тому, что делает реальный DI-контейнер в продакшене: графы разрешения, циклические зависимости, жадный старт и когда не использовать контейнер вовсе.
встречается в185
- Задачи, микрозадачи и scheduler.yield()middle
- Точность таймеров, троттлинг и фоновая работаmiddle
- Event loop Node.js: фазы, nextTick и задержка циклаsenior
- Стратегии рендеринга: SSG, SSR, ISR, streaming и гидратацияjunior
- SSG, SSR, ISR, streaming и RSC — как работает каждая стратегияmiddle
- Цена гидратации: selective, progressive, острова, resumabilitymiddle
- Core Web Vitals: что измеряют LCP, INP и CLSjunior
- LCP: четыре фазы, одна доминирующая стоимостьmiddle
- INP: input delay, processing, presentationmiddle
- Lab vs field: почему они расходятся и как использовать каждыйmiddle
- Трейдоффы метрик, RUM-атрибуция и цикл CI+полеsenior
- Общая картина: от URL до LCP до INP как эстафетаjunior
- Восемь слоёв трассировки: от service worker до второй навигацииmiddle
- Пять канонических поломок: где производство стабильно ломаетсяsenior
- Метод трёх треков: чтение трасс и построение системы мониторингаsenior
- Что такое индекс и как он ускоряет запросыjunior
- Leading-column rule: почему порядок столбцов в composite-индексе важенmiddle
- Partial, expression и covering-индексыmiddle
- Типы индексов: GIN, GiST, BRIN, Hash, Bloom и HOT-обновленияmiddle
- Index-only scan, Visibility Map и INCLUDEsenior
- Типичные сбои в продакшне и аудит индексовsenior
- Упражнение по проектированию индексов: стратегия полнотекстового поискаsenior
- EXPLAIN и планы выполнения: что решает планировщик и почемуjunior
- Типы сканирования: Seq, Index, Bitmap, Index-Onlymiddle
- Алгоритмы соединения и каскад ошибок оценки строкmiddle
- pg_statistic, ANALYZE и производственная наблюдаемостьmiddle
- Расширенная статистика: исправление ошибок оценки для коррелированных колонокsenior
- Кеш планов, настройка константных стоимостей и внутренности планировщикаsenior
- Производственные режимы отказа и стабильность плановsenior
- Connection pool: зачем амортизировать стоимость backend Postgresjunior
- Режимы PgBouncer: session, transaction и statementmiddle
- Размер пула: формула (ядра × 2) + шпинделей и двухуровневый стекmiddle
- Исчерпание пула и idle-in-transaction: сценарий отказа в 3 ночиmiddle
- Миграция на transaction mode: план развёртывания и prepared statements в PgBouncer 1.21middle
- Процессная модель Postgres и почему увеличение max_connections снижает производительностьsenior
- Ландшафт пулеров 2026, serverless connection storms и полная таксономия отказовsenior
- ADD COLUMN: мгновенно в PG 11+ против перезаписи в старом Postgresjunior
- Режим отказа очереди блокировок: почему мгновенный DDL может заморозить базуmiddle
- Безопасные DDL-паттерны: NOT VALID, CONCURRENTLY и исправления небезопасных операцийmiddle
- Таксономия сбоев миграций и дисциплина продакшнаsenior
- Выбор ключа шарда: стратегии hash, range, list и directorymiddle
- Ко-локация и Citus: инвариант, делающий шардирование пригодным к использованиюmiddle
- Режим отказа hot shard: обнаружение, изоляция и долгосрочная политикаmiddle
- Онлайн-решардинг, 2PC и операционная стоимость шардированияsenior
- Семь актов: от CREATE TABLE до Citusjunior
- Акты 1–3 в глубину: схема, индексы и статистика планировщикаmiddle
- Акты 4–6 в глубину: MVCC bloat, connection pooling и безопасные миграцииmiddle
- Акт 7 в глубину: шардинг, co-location и семиуровневый каскад трейдоффовmiddle
- Наблюдаемость, антипаттерны и производственный триажsenior
- Биты в проводеjunior
- Математика задержкиmiddle
- Bufferbloat и перегрузкаsenior
- Граница физического уровняsenior
- Номера последовательности и состояние соединенияmiddle
- Управление потоком и перегрузкойmiddle
- BBR, производственная наблюдаемость и за пределами TCPsenior
- CDN: контент по соседствуjunior
- Anycast и GeoDNS: маршрутизация к ближайшему edgemiddle
- Многоуровневый кеш и Cache-Controlmiddle
- Заголовок Vary и cache keysmiddle
- Stale-while-revalidate и cache stampedesenior
- Edge workers и edge-side compositionsenior
- CDN: операции и observabilitysenior
- WebSocket: HTTP-апгрейд до постоянного соединенияjunior
- WebSocket vs SSE vs long-polling: выбор правильного транспортаmiddle
- Backpressure в WebSocket: когда клиенты не успеваютmiddle
- Реконнект: jittered backoff, thundering herd, восстановление сообщенийsenior
- WebSocket в масштабе: HTTP/2 мультиплексирование, permessage-deflate, C10Msenior
- WebSocket в production: прокси, безопасность и распределённая архитектураsenior
- Что делают обратные проксиjunior
- Алгоритмы балансировки: от round-robin до power-of-two-choicesmiddle
- L4 vs L7 балансировка и сохранение IP клиентаmiddle
- Health checks, connection draining и slow startmiddle
- Retry-бури, circuit breakers и load sheddingsenior
- Устойчивая архитектура LB: anycast, zone-aware маршрутизация и observabilitysenior
- Почему QUIC, а не TCP+TLSjunior
- QUIC-потоки и head-of-line blockingjunior
- Объединённое рукопожатие и 1-RTTmiddle
- Connection ID и миграция сетиmiddle
- Обнаружение потерь и управление перегрузкойmiddle
- Возобновление 0-RTT и шифрование пакетовsenior
- Развёртывание и стоимость CPUsenior
- DDoS: что это и почему работаетjunior
- Атаки усиления и истощение состоянияmiddle
- Ограничение скорости: алгоритмы и архитектураmiddle
- WAF, межсетевые экраны, mTLS и HSTSmiddle
- Отравление DNS-кэша и BGP-перехватsenior
- Эшелонированная защита и экономика атакsenior
- Двенадцать слоёв: один URL, семь действующих лицjunior
- DNS, TCP, TLS по очереди: куда уходят миллисекундыmiddle
- Критический путь рендеринга и Core Web Vitalsmiddle
- Перехват прокси и шлюзы безопасности: rate limiter, WAF, mTLSmiddle
- Альтернативные пути: QUIC 0-RTT, WebSocket upgrade, миграция соединенияmiddle
- Наблюдаемость: распределённые трейсы, USE/RED и семплированиеsenior
- Устойчивость: каскадные повторы, circuit breakers и error budgetsenior
- Что такое три сигнала: метрики, логи, трейсыjunior
- Метрики и cardinality: cost-модель time-series databasemiddle
- Логи и объём: cost-модель структурного логированияmiddle
- Трейсы и сэмплирование: cost-модель distributed tracingmiddle
- Join-ключи и exemplar''''ы: как три сигнала становятся компонуемымиmiddle
- Observability 2.0: широкие события и сдвиг стоимостиsenior
- Режимы сбоя и инженерная практика: cardinality budget''''ы, PII и сэмплированиеsenior
- Зачем нужны структурные логи: дневник против таблицыjunior
- Схема продакшн-лога: поля, которые несёт каждая строкаmiddle
- Log levels и маршрутизация алертовmiddle
- Стратегии sampling и стоимость логовmiddle
- PII-редакция и log injectionsenior
- Propagation trace-контекста в логахsenior
- OTel Logs Data Model и audit-логи как подсистемаsenior
- Сигналы OTel, Semantic Conventions и проводной формат OTLPmiddle
- Авто-инструментирование и ручные спаны: правило 80/20 в OTelmiddle
- Collector OTel: receivers, processors, exporters и паттерны развёртыванияmiddle
- Стратегии сэмплирования: head, tail и parent-basedmiddle
- Vendor-нейтральность, eBPF-инструментирование, Operator и OTel в браузере и serverlesssenior
- Эксплуатация OTel Collector: надёжность, version skew, режимы отказа и управлениеsenior
- RED и USE: два чек-листа, одна дисциплина триажаjunior
- Инструментация RED в Prometheus: счётчики, гистограммы и дисциплина cardinalitymiddle
- USE на Linux: CPU, память, диск, сеть и PSImiddle
- Golden signals, структура дашборда и auto-RED в service meshmiddle
- Cardinality как драйвер затрат: label, PII, exemplars и семплированиеmiddle
- Native histograms, SLO и паттерны production-сбоевmiddle
- Выбор SLI и SLO-целей: отношения, не ощущенияmiddle
- Multi-window multi-burn-rate-алертинг: почему AND лучше ORmiddle
- Error budget policy, latency SLO и составные journeysmiddle
- Iceberg SLI, математика составного SLO и SLA vs SLOsenior
- Flame graph: читаем картинку, которая показывает, куда ушло времяjunior
- Sampling vs instrumentation profiling: почему 99 Гц побеждает в productionmiddle
- Типы профилей: CPU, память, off-CPU, mutex — какой когда братьmiddle
- Continuous profiling: always-on flame graphs с eBPF и корреляцией trace-idmiddle
- Как flame graph строится из сэмплов и как использовать его в productionmiddle
- Linux perf, внутренности eBPF, PGO и ограничения sampling''''аsenior
- Profiling в production: безопасность, war stories, OTel profiles и дизайн инфраструктурыsenior
- Debugging-воронка: SLO → RED → trace → profilejunior
- Архитектура OTel: один SDK, четыре сигнала, один wire-форматmiddle
- Экономия на observability: удерживаем затраты в пределах 5% inframiddle
- Масштаб, безопасность и ROI наблюдаемых системsenior
- Сначала профиль: измерь куда реально уходит времяjunior
- Закон Амдала и self-time: потолок любого ускорения, которое ты можешь выпуститьmiddle
- Измерительный цикл: микробенч, макробенч, prod-профиль, эффект наблюдателяmiddle
- Чтение флейм-графов: формы, профайлеры по языкам и 60-секундный сканmiddle
- Статистические baseline''''ы: почему один запуск — не измерениеmiddle
- История профайлеров и ловушки микробенчей: от Кнута до GWPsenior
- Hardware counters, профили холодного старта и безопасность профилейsenior
- Непрерывное профилирование в масштабе: затраты, CI-гейты, корреляция с трейсами и антипаттерныsenior
- Что делает путь горячим: симптом против причиныjunior
- Пять форм hotspot''''а: CPU, аллокации, кэш, лок, syscallmiddle
- Чтение parent и child chains: где применять правкуmiddle
- JIT deopt, цикл fix-and-verify и PR-time профилированиеmiddle
- Аппаратные счётчики и Intel TMA: диагностика подкатегорийsenior
- False sharing и горячие пути нативных мостовsenior
- Горячие пути в production: безопасность, хвостовая латентность и происхождение инструментовsenior
- Иерархия памяти: почему расстояние важнее числа операцийjunior
- Row-major vs column-major: порядок доступа и разрыв в 9xjunior
- Branch prediction: 10–30 циклов штрафа за неожиданный ifmiddle
- Hardware prefetcher, TLB и memory-level parallelismsenior
- Основы GC: за что рантайм берёт налогjunior
- Алгоритмы GC: поколенческая гипотеза, concurrent marking и write barriermiddle
- GC tradeoffs: пауза, throughput, память и давление аллокацийmiddle
- Настройка GC: пейсинг, форма кучи и наблюдаемость аллокацийmiddle
- Внутреннее устройство GC: tri-color инвариант, write barriers и глубокое погружение в рантаймыsenior
- GC в production: наблюдаемость, безопасность, edge cases и управление флотомsenior
- N+1: одна логическая операция, много round-trip''''овjunior
- Семейства фиксов: JOIN, IN, preload и DataLoadermiddle
- Обнаружение N+1: query logs, APM traces и CI gatesmiddle
- DataLoader: батчинг по дереву резолверовmiddle
- Кросс-протокольный N+1: HTTP fan-out и Redis MGETmiddle
- N+1 в масштабе: исчерпание пула, изменения планов и денормализацияsenior
- Batching: амортизируй фиксированную цену каждой операцииjunior
- Окно батчинга: размер и время ожиданияmiddle
- Batching в Kafka и Postgresmiddle
- io_uring и наблюдаемость пакетированияmiddle
- От Nagle до io_uring: эволюция пакетированияmiddle
- Backpressure, изоляция сбоев и безопасность батчей в продакшенеsenior
- Что на самом деле стоит bundle: download, parse, compile, executejunior
- Core Web Vitals: LCP, INP и CLSmiddle
- Code splitting: route-level, component-level, vendor splittingmiddle
- Tree shaking и compression: удаляем то, что не используемmiddle
- Third-party scripts: тихий убийца бюджетаmiddle
- CI enforcement и RUM: делаем бюджеты рабочимиmiddle
- V8 JIT-пайплайн, HTTP-приоритеты и безопасность bundlesenior
- Цикл performance: дисциплина, а не проектjunior
- Классификация и исправление: сопоставление family bottleneck с методамиmiddle
- Observability-стек и CI gates: ловить регрессии до выпускаmiddle
- От инцидента к enforcement: SLO burn до верифицированного исправления за 35 минутmiddle
- Культура, экономика и масштаб performancesenior