Сервис вызывает mock-зависимость из нескольких горутин. Как сделать проверку вызовов корректной?
Состояние mock-объекта, в котором записываются вызовы, нужно защищать синхронизацией: например, мьютексом или потокобезопасной структурой. Проверять вызовы следует только после того, как завершились все горутины, иначе тест может читать журнал одновременно с записью или увидеть неполный результат.
Mock-объекты появились как способ изолировать unit-тест от внешних зависимостей и проверять взаимодействие с ними. Изначально многие mock-реализации использовались в последовательных тестах, где обычного среза вызовов было достаточно.
С появлением конкурентных сервисов одна и та же зависимость стала вызываться из нескольких горутин. Прежний подход оказался небезопасным: сам mock мог стать источником гонки и сделать результат теста недостоверным.
Если mock добавляет записи в общий срез или изменяет map без синхронизации, параллельные вызовы могут привести к гонке данных, повреждению состояния или потере части записей. Даже если тест иногда проходит, race detector может обнаружить проблему, а поведение при обычном запуске останется недетерминированным.
Отдельная ошибка — проверять журнал сразу после запуска горутин. Защищённый мьютексом mock всё равно не гарантирует, что все вызовы уже произошли: синхронизация защищает доступ к данным, но не ожидает завершения работы.
В mock нужно синхронизировать каждую операцию над общим состоянием: добавление вызова, чтение журнала и изменение счётчиков. Обычно запись выполняется под sync.Mutex, а метод чтения делает копию данных под тем же мьютексом, чтобы вызывающий код не работал с внутренним срезом после разблокировки.
В полном примере требуется импортировать sync. Копирование в Calls важно: возврат внутреннего среза позволил бы внешнему коду читать или изменять его без защиты.
Ожидание завершения горутин должно быть отдельной частью теста, обычно через sync.WaitGroup, канал завершения или другой явный протокол. Нельзя заменять это time.Sleep: задержка не устанавливает причинно-следственную связь и делает тест зависимым от нагрузки.
Мьютекс прост и надёжен, но может усложнить mock и скрыть чрезмерную конкурентность тестируемого кода. Каналы подходят, если журналирование естественно строится как передача событий, однако для обычного накопления вызовов они часто избыточны. Готовый mock-фреймворк допустим только при условии, что его документация явно описывает безопасность параллельных вызовов.
Сервис параллельно загружал данные для нескольких клиентов и использовал mock хранилища, который добавлял ключи в обычный срез. В последовательном запуске тест проходил, но при -race появлялись отчёты о конфликтующих доступах, а иногда число записей отличалось от ожидаемого.
Рассматривались три варианта: убрать параллельность из сервиса, поставить time.Sleep перед проверкой или сделать mock потокобезопасным и явно дождаться всех рабочих горутин. Первый вариант изменял проверяемое поведение, второй не устранял гонку и оставался нестабильным.
Выбрали третий вариант: журнал mock защищали мьютексом, метод чтения возвращал копию, а тест завершал WaitGroup перед проверкой. В результате проверка стала детерминированной, а go test -race перестал сообщать о гонке в тестовом коде.
Нет. Чтение журнала и любые операции над связанными полями также должны использовать тот же механизм синхронизации. Иначе запись будет защищена, но параллельная проверка или вычисление длины среза всё равно создаст гонку.
Нет. Потокобезопасность гарантирует согласованный доступ к состоянию, но не завершение горутин. Тест должен отдельно дождаться всех операций; только после этого можно сравнивать количество, аргументы и порядок вызовов.
Только если порядок является частью контракта и сам тест задаёт соответствующее упорядочивание. При независимых параллельных операциях глобальный порядок может быть случайным, поэтому обычно проверяют множество вызовов или свойства каждого вызова, а не конкретную последовательность.