Согласно классическому определению Майкла Физерса (Michael Feathers, Working Effectively with Legacy Code), легаси-код — это код, не покрытый автоматизированными тестами. При работе с такими системами инженер сталкивается с фундаментальной дилеммой рефакторинга (Refactoring Dilemma): чтобы безопасно изменить или декомпозировать код, требуются тесты; однако чтобы написать тесты, код уже необходимо изменить, разорвав жесткие скрытые зависимости (Tight Coupling).
Попытка вручную внедрить модульные юнит-тесты в процедурный код с глобальным состоянием ($GLOBALS, global $wpdb, синглтоны Instance::get(), статические фабрики) приводит к экспоненциальным трудозатратам. Разработчик вынужден вручную конструировать моки для сотен неявных зависимостей, рискуя нарушить исходное поведение системы еще до фиксации первого успешного прогона.
Инженерным решением 2026 года является ретрофиттинг тестов (Test Retrofitting) через паттерн Golden Master (Approval Testing) в связке с комбинаторным фаззингом (Combinatorial Fuzzing) с помощью AI-агентов. Агент анализирует пространство входных параметров, синтезирует репрезентативную матрицу краевых значений, оборачивает систему изолирующими прокси-швами (Seams) и фиксирует сотни детерминированных снимков (Snapshots) ввода-вывода. Полученный эталонный набор гарантирует нулевую регрессию при последующем архитектурном распиле.
Пайплайн ретрофиттинга изолирует недетерминированные факторы среды и фиксирует эталонное состояние системы до начала вмешательства в исходный код:
[Combinatorial Input Matrix Generator (AI Fuzzer)]
│
▼
┌────────────────────────────────────────┐
│ Seam & Proxy Isolation Layer │
│ - Virtual System Clock (Fixed time()) │
│ - PRNG Seed (Fixed mt_rand()) │
│ - Global State Capture ($GLOBALS) │
└────────────────────────────────────────┘
│
▼
[Legacy SUT (System Under Test)]
│
▼
[Output Interceptor (STDOUT, DB)]
│
▼
[Golden Master Snapshot Store]
(Canonical JSON / State Hashes)
│
(Refactoring / Porting phase)
│
▼
[Equivalence Regression Engine]
Assert: Output(Refactored) === GoldenMaster$wpdb, $post, $current_user через специализированные тестовые швы (Brain Monkey / Patchwork) для создания Golden Master снепшотов.LegacyDatabase::query() без правки сотен зависимых файлов.Рассмотрим legacy-класс расчета налоговой ставки, жестко привязанный к глобальным переменным, статическому синглтону реестра и системному времени:
<?php
// Legacy-код с критическими антипаттернами тестируемости
class TaxCalculator {
public function compute($amount, $regionCode, $isB2B) {
global $wpdb;
// Антипаттерн 1: Недетерминированное системное время
$currentHour = (int)date('H');
// Антипаттерн 2: Жесткая зависимость от синглтона
$config = LegacyRegistry::getInstance()->get('tax_rules');
// Антипаттерн 3: Прямой SQL-запрос к глобальной базе данных
$row = $wpdb->get_row("SELECT surcharge FROM region_taxes WHERE code = '" . addslashes($regionCode) . "'");
$surcharge = $row ? (float)$row->surcharge : 0.05;
$rate = 0.20; // 20% базовая ставка
if ($isB2B) {
$rate = 0.0; // B2B реверс
} else {
// Ночная наценка между 23:00 и 06:00
if ($currentHour >= 23 || $currentHour < 6) {
$rate += 0.02;
}
$rate += $surcharge;
}
$taxAmount = round($amount * $rate, 2);
// Сайд-эффект: запись в глобальный массив аудита
$GLOBALS['AUDIT_LOG'][] = [
'region' => $regionCode,
'tax' => $taxAmount,
'timestamp' => time()
];
return $taxAmount;
}
}Для изоляции системных вызовов создается тестовый прокси-слой, перехватывающий время, базу данных и глобальное состояние:
<?php
declare(strict_types=1);
/**
* Подкласс-шпион (Subclass to Test / Seam Pattern).
* Позволяет подменять системные вызовы без модификации продакшен-логики.
*/
class TestableTaxCalculator extends TaxCalculator {
private int $frozenHour = 12;
private array $mockDbRows = [];
public function setFrozenHour(int $hour): void {
$this->frozenHour = $hour;
}
public function setMockRegionSurcharge(string $region, float $surcharge): void {
$this->mockDbRows[$region] = (object)['surcharge' => $surcharge];
}
/**
* Переопределение точки замера времени (Time Seam)
*/
protected function getCurrentHour(): int {
return $this->frozenHour;
}
}Агент на TypeScript анализирует границы параметров и генерирует исчерпывающий массив тестовых кейсов для Approval Testing:
import { z } from 'zod';
// Схема декартова произведения входных данных
export const TestCaseSchema = z.object({
amount: z.number(),
regionCode: z.string(),
isB2B: z.boolean(),
mockHour: z.number().int().min(0).max(23),
mockSurcharge: z.number(),
});
export type TestCase = z.infer<typeof TestCaseSchema>;
export function generateCombinatorialMatrix(): TestCase[] {
// Граничные значения для каждого измерения
const amounts = [0, 0.01, 100, 999.99, 1000000];
const regions = ['EU', 'US', 'DEFAULT', 'INVALID_EMPTY'];
const b2bOptions = [true, false];
const hours = [3, 12, 23]; // Ночь, день, пограничный час
const surcharges = [0.0, 0.05, 0.15];
const matrix: TestCase[] = [];
for (const amount of amounts) {
for (const regionCode of regions) {
for (const isB2B of b2bOptions) {
for (const mockHour of hours) {
for (const mockSurcharge of surcharges) {
matrix.push({
amount,
regionCode,
isB2B,
mockHour,
mockSurcharge,
});
}
}
}
}
}
// Сгенерировано 5 * 4 * 2 * 3 * 3 = 360 уникальных комбинаций
return matrix;
}Сгенерированные снимки сериализуются в нормализованный JSON, исключающий динамический шум (хеши, случайные ID):
import { describe, it, expect } from 'vitest';
import { generateCombinatorialMatrix } from '@/scripts/generate-golden-matrix';
// Имитация детерминированного вызова портированного модуля
import { computeTaxSafe } from '@/lib/tax-engine';
describe('Golden Master: Tax Calculation Regression Suite', () => {
const cases = generateCombinatorialMatrix();
it('полностью совпадает с эталонными снимками Golden Master на 360 сценариях', () => {
const results = cases.map((testCase) => {
// Инициализация чистого контекста аудита
const auditLog: Array<{ region: string; tax: number }> = [];
const result = computeTaxSafe({
amount: testCase.amount,
regionCode: testCase.regionCode,
isB2B: testCase.isB2B,
currentHour: testCase.mockHour,
surcharge: testCase.mockSurcharge,
onAudit: (entry) => auditLog.push(entry),
});
return {
input: testCase,
output: result,
auditState: auditLog,
};
});
// Сравнение всего массива состояний со снепшотом Vitest
expect(results).toMatchSnapshot();
});
});Использование неконтролируемых вызовов time(), uniqid(), microtime() или случайных значений rand() приводит к тому, что снепшот Golden Master падает при каждом перезапуске без фактических изменений в коде. Решение: обязательная заморозка времени через виртуальные часы (Virtual Clock / Seams) до снятия снепшота.
Классическая ошибка: разработчик начинает рефакторинг с наведения порядка в классах, чтобы облегчить тестирование. Любое исправление даже явного бага до фиксации эталона лишает вас объективной точки отсчета. Сначала снимите снепшот фактического поведения (включая баги), и только потом рефакторите.
Попытка зафиксировать весь дамп базы данных или полный HTML-вывод со сторонними баннерами раздувает репозиторий и делает diff нечитаемым при проверке пул-реквестов. Фиксируйте строго нормализованные проекции состояния (State Projections): возвращаемое значение + критические строки затронутых таблиц.
Если фаззер проверяет только положительные круглые числа (100, 200), но не подает 0, 0.0001, отрицательные значения и спецсимволы в строках (NULL, \0, ' OR 1=1), скрытые ветвления останутся непокрытыми, а рефакторинг приведет к регрессионным сбоям.
Обнаружение точек перехвата зависимостей (точки инстанцирования, I/O, системное время).
Оборачивание глобального состояния ($wpdb, $GLOBALS) в тестовые обертки без правки бизнес-кода.
ИИ-агент генерирует матрицу краевых значений параметров через декартово произведение.
Исполнение сотен тестов на legacy-рантайме и сохранение канонического файла снимков (Snapshots).
Безопасное переписывание кода, выделение чистых классов и устранение запахов кода.
Автоматическая сверка вывода отрефакторенного кода с Golden Master с точностью до бита.