Объясните механизм: почему mock, сохранивший указатель на переданный объект, может проверить уже изменённое состояние аргумента, а не состояние на момент вызова?
Потому что mock сохраняет ссылку на тот же объект, а не независимую копию его состояния. Если код после вызова изменит объект, последующая проверка mock увидит изменённые данные и может ошибочно подтвердить или отклонить вызов.
Чтобы проверять состояние именно на момент вызова, mock должен сохранить снимок аргумента сразу при получении либо выполнить проверку непосредственно внутри обработчика вызова.
Mock-объекты появились как средство изолированного тестирования взаимодействий: тест должен был проверять не только результат, но и передачу зависимости корректных аргументов. При этом ранние и самописные mock-реализации часто просто сохраняют полученные значения для последующей проверки.
Такой подход удобен, но не устраняет семантику ссылочных типов Go. Указатель, срез или map могут продолжать указывать на изменяемое состояние после завершения вызова зависимости.
Представим сервис, который передаёт запрос в mock, а затем переиспользует этот запрос и изменяет его поля. Если mock запомнил указатель, отложенная проверка анализирует не исторический факт вызова, а текущее содержимое объекта.
Это создаёт ложные результаты: тест может упасть из-за изменений, не относящихся к вызову, или, наоборот, пройти, потому что поздняя мутация случайно привела аргумент к ожидаемому виду. Проблема особенно заметна при асинхронных вызовах, переиспользовании буферов и проверках после завершения нескольких операций.
В Go передача указателя копирует адрес, но не объект. Поэтому mock, сохранивший такой указатель, разделяет состояние с тестируемым кодом. Аналогично, копирование заголовка среза не копирует его массив; map также остаётся общей изменяемой структурой.
Минимальный пример:
Надёжные варианты — копировать данные при записи в mock или проверять их внутри callback, который выполняется в момент вызова. Копирование должно быть глубоким, если структура содержит указатели, срезы или map; поверхностная копия структуры не гарантирует независимость вложенных данных.
Проверка в момент вызова лучше сохраняет временную семантику, но усложняет диагностику и синхронизацию. Снимок удобнее для отложенных assertions, однако требует явно определить, какие поля и вложенные объекты нужно копировать. Если контракт допускает изменение объекта после вызова, проверять конкретное содержимое вообще не следует — достаточно проверить идентичность или факт вызова.
Сервис формировал запрос, передавал его в mock HTTP-клиента, а затем очищал поля запроса перед возвратом объекта в пул. Mock хранил указатель, поэтому проверка иногда видела пустой URL; при параллельных тестах результат становился нестабильным.
Рассматривались три варианта. Проверять указатель на идентичность было слишком слабо: это подтверждало передачу объекта, но не его содержимое. Добавить Sleep или изменить порядок очистки устраняло отдельный симптом, но оставляло ошибочную модель теста. Глубокое копирование запроса в mock дало стабильный снимок и сохранило возможность подробной диагностики.
Выбранное решение — копировать значимые поля при входе в mock и отдельно тестировать корректность жизненного цикла объекта. Это устранило зависимость проверки от последующих мутаций, но потребовало обновлять функцию копирования при добавлении новых ссылочных полей.
Нет. Такая копия независима только для полей-значений. Указатели, срезы, map, массивы, содержащие ссылочные элементы, и интерфейсы с изменяемым конкретным значением могут по-прежнему разделять внутреннее состояние. Глубина копирования должна соответствовать контракту и цели проверки.
Не обязательно исчезнет. Интерфейс копирует пару «тип и значение», но если значением является указатель, срез или map, копируется ссылка на изменяемые данные. Поэтому хранение аргумента в any само по себе не создаёт снимок.
Нет. Копирование аргумента защищает от последующих изменений исходного объекта, но не делает внутреннее хранилище mock потокобезопасным. При вызовах из нескольких горутин нужны синхронизация доступа к истории вызовов и согласованная стратегия проверки; кроме того, копирование должно происходить до того, как другая горутина может изменить переданный объект.