Интеграционное тестирование смарт-контрактов и его слепые зоны
Модульные проверки подтверждают, что отдельный контракт работает по своей спецификации. Ценность протокола рождается на стыках — между токеном и хранилищем, между расчётом и выплатой, между логикой и внешним источником данных. Интеграционное тестирование смарт-контрактов проверяет связку целиком в том порядке вызовов, в котором её увидит сеть Ethereum внутри одной транзакции.
Отказы на стыках поштучно почти не видны: получатель считает, что оплата прошла, отправитель уже зафиксировал списание, а между ними сработал возврат. Рассинхрон состояний всплывает позже, когда по расходящимся данным считают лимиты, доли или вознаграждения, и восстанавливать приходится вручную.
Отдельная зона — порядок вызовов и повторные входы. Если выплата уходит внешнему адресу до обновления баланса, получатель успевает вызвать функцию заново и вывести больше, чем ему разрешено. Такая защита проверяется только на уровне цепочки вызовов, а не одного контракта.
Пропущенные ветки сценариев — вторая типичная дыра. Проверяют успешный путь и полный отказ, но не частичный: сбой на середине откатывает транзакцию целиком, а внешний сервис уже получил событие и записал операцию в свою базу. Расхождение между сетью и учётной системой обнаруживается на сверке, то есть слишком поздно.
Инциденты после обновления стоят дороже всего: логику меняют в новом контракте, а смежные модули продолжают обращаться к старому, и часть состояния остаётся неучтённой. Разбираем, что именно проверяем на связке и как ловим эти расхождения до выката.
Что даёт проверка сквозных сценариев до выкладки в основную сеть
Сквозной сценарий прогоняет всю цепочку — от вызова публичного метода до изменения состояния и события, которое читает внешний сервис. Мы поднимаем изолированный стенд с локальной копией состояния основной сети, фиксируем номер блока и разворачиваем всю связку контрактов. Это даёт воспроизводимость: один и тот же набор транзакций на одном снимке состояния даёт идентичный результат у любого инженера и в любой сборочной среде.
Полный набор сценариев проходит за минуты вместо часов ручных проверок: параллельные прогоны на копиях стенда не мешают друг другу, а состояние откатывается снимком. Повторяемость результатов — главный эффект: падение означает реальную ошибку в логике, а не разницу в балансе тестового аккаунта или времени блока.
Так снимаются целые классы отказов: расхождение прав между ролями, неверный порядок вызовов при переводах между контрактами, повторный вход, переполнение при расчёте долей, несовпадение событий и фактического состояния. Доля дефектов, доходящих до пользователя, падает — они ловятся там, где исправление занимает часы, а не разбор инцидента в основной сети.
Обновление проверяется тем же контуром: сценарий применяет новую версию к снимку текущего состояния и прогоняет миграцию. Видно, что старые данные читаются корректно, а время простоя при обновлении ограничивается одной транзакцией замены, а не поиском причины сбоя после неё.
Форматы прогонов: локальный стенд, закрытая сеть, основная сеть
Выбор окружения определяет, какие дефекты прогон вообще способен поймать. Локальный стенд даёт полную изоляцию тестовых данных и секунды на цикл, но не воспроизводит поведение реальных пулов ликвидности, оракулов и загрузки сети. Закрытая сеть с теми же контрактами и снимками состояния закрывает середину: поведение близко к боевому, а данные не пересекаются с рабочими.
Что сравниваем
| Вариант | Когда подходит | Ограничения окружения |
|---|---|---|
| Локальный стенд | Логика контракта, права доступа, границы значений, регресс после каждого изменения кода | Нет реальной загрузки, внешние вызовы подменяются заглушками |
| Закрытая сеть | Сценарии между несколькими контрактами, миграции состояния, условия, зависящие от времени | Нужно поднимать и сопровождать узлы, состояние пересобирается под каждый прогон |
| Прогон в основной сети | Финальная проверка перед запуском: реальные внешние вызовы, достоверная оценка расхода газа | Транзакции необратимы, изоляции тестовых данных нет — только чтение или отдельные аккаунты |
Основной критерий выбора — изоляция тестовых данных. Прогон в основной сети оправдан, когда проверяется стыковка с внешними контрактами и фактическая стоимость операций, а не логика. Закрытая сеть обычно становится рабочим режимом для интеграционных сценариев: она единственная даёт и достоверность, и возможность откатить состояние.
Ограничения задаёт и окружение: разные версии узлов, лимиты точек доступа и тайминги включения блоков могут расходиться, поэтому наборы проверок мы держим раздельными под каждый режим.
Этапы работ: от разбора сценариев до тестирования на стенде
Интеграционное тестирование смарт-контрактов начинается не с кода тестов, а с разбора связки: как контракты вызывают друг друга, что ждут от внешних сервисов и в каком состоянии остаётся система после каждой операции. Ошибка в порядке вызовов или в ожидаемом состоянии соседнего контракта проявляется уже после развёртывания, когда исправление требует миграции. Поэтому порядок работ у нас такой: сначала карта связки, затем тесты.
- Аудит сценариев — собираем пользовательские пути и роли: кто вызывает метод, какие лимиты и состояния при этом проверяются, где ожидаются отказы. На выходе — перечень сценариев с приоритетом по риску.
- Карта вызовов — фиксируем связи между контрактами, токенами и внешними источниками данных: кто кого вызывает, какие значения передаёт и что возвращает. Карта сразу показывает места, где тест обязан проверять и побочные изменения состояния.
- Схема сценариев — каждый путь превращаем в набор шагов с ожидаемым состоянием до и после вызова, чтобы расхождение читалось точечно, а не как «упал весь прогон».
- Настройка окружения — стенд повторяет целевую сеть по параметрам комиссий, порядку подтверждений и версиям зависимостей; отдельно поднимаем тестовую сеть для проверок, которые локально не воспроизводятся.
- Прогон и разбор расхождений — запускаем сценарии пакетами, логируем входы, состояние и события, каждое расхождение доводим до причины: ошибка в контракте, в окружении или в самом ожидании теста.
- Критерии приёмки — до финального прогона согласуем, какие сценарии обязательны и какой результат считаем успешным, чтобы приёмка не превратилась в переговоры.
- Передача — отдаём набор тестов, конфигурации стенда и отчёт с покрытием путей, чтобы прогон повторялся у вашей команды без нас.
Артефакты на выходе: наборы сценариев, отчёты, регламенты
Передаём не папку с тестами, а рабочий контур: запускаемый набор сценариев, воспроизводимые данные и правила, по которым ваша команда сама прогоняет и обновляет проверки после каждой правки контрактов. Ниже — что физически остаётся у вас в репозитории и в документации.
- Набор сценариев интеграционного тестирования — сгруппирован по доменам: токен, доступы, выплаты, внешние вызовы. Каждый сценарий запускается отдельно, поэтому упавшая проверка указывает на конкретную функцию, а не на «что-то сломалось».
- Фикстуры и документация по данным — описано, из какого состояния поднимается стенд: снимок основной сети на нужной высоте блока, профинансированные адреса, предустановленные роли. Стенд поднимается одной командой и даёт одинаковый результат у разработчика и на сервере сборки.
- Схема взаимодействия модулей — карта вызовов между контрактами и внешними сервисами с пометкой, что покрыто проверками, а что осталось на ручном контроле.
- Отчёт о прогоне — пройденные и упавшие сценарии, время выполнения, изменение расхода газа относительно прошлого запуска, покрытие ветвей. Регресс виден до выкладки, а не после.
- Регламент обновления — обязательный набор сценариев при правке контракта, кто подтверждает прогон, как фиксируется новая версия и как откатиться на предыдущую.
- Роли и доступы — матрица прав на изменение сценариев, ключи тестовых сетей, разделение доступа к стенду и к боевым конфигурациям.
Кейс: как ушёл класс отказов при выпуске обновления
Заказчик выпускал обновления связки из шести контрактов: токен, пул, оракул, модуль голосования и два вспомогательных. Модули тестировали по отдельности, а совместное поведение проверяли руками на тестовой сети — выпуск занимал двое суток, и примерно треть релизов приходилось откатывать, потому что несовместимость интерфейсов всплывала уже после публикации.
Что изменили
Собрали единый прогон: фикстуры разворачивают всю связку в изолированной сети, включая обновление с сохранением накопленного хранилища, поэтому проверяется не только новый код модуля, но и совместимость с уже записанными данными. Добавили проверки инвариантов — сумма долей, права ролей, лимиты выпуска — они прогоняются после каждой операции, а не один раз в конце. Сценарий отката на предыдущую версию стал частью прогона, а не аварийной процедурой по инструкции.
Разворачивание и запуск построены на Hardhat: те же сценарии выполняются у инженера локально и в сборке — расхождений между средами нет.
| Метрика | До | После |
|---|---|---|
| Доля релизов с откатом | каждый третий | один из четырнадцати |
| Время полной проверки связки | двое суток вручную | около сорока минут |
| Время отката на предыдущую версию | три-четыре часа | до десяти минут одним сценарием |
Теперь релиз не ждёт окна в конце недели: прогон стартует на каждое изменение и падает на несовместимости до публикации. Откат перестал быть инцидентом с ручными шагами — это тот же сценарий из прогона, выполненный на боевой сети.
Что делать, если связка уже развёрнута и работает в основной сети?
Живая система отличается тем, что остановить её для проверки нельзя: балансы и хранилище меняются под руками. Проверка строится на копии — снимок состояния на фиксированном блоке разворачивается в изолированном узле, где воспроизводится любая последовательность вызовов без риска для основной сети. Этот же снимок служит точкой сравнения: после прогона сверяем балансы, события и хранилище с ожидаемыми значениями.
Второй контур — теневой прогон: новая версия логики принимает те же вызовы, но результат никуда не отправляется и сравнивается с ответом работающего контракта. Расхождение хотя бы в одном сценарии означает несовместимость, и сборка до сети не доходит. Внешние зависимости — оракулы, пулы, контракты партнёров — подменяются управляемыми заглушками с заданным поведением, включая отказы и задержки ответа.
Отдельно проверяем обратную совместимость обновлений: старые записи в хранилище должны читаться новой логикой, а выпущенные события — остаться понятными индексаторам и кошелькам. В обновляемых контрактах порядок полей и слоты хранения фиксируем заранее на схемах OpenZeppelin: новое поле добавляется только в конец. Так уходит класс отказов, при которых после обновления обнуляются балансы или ломаются права доступа.
Выпуск идёт поэтапно: снимок, теневой прогон на копии, ограниченный запуск на доле трафика с лимитами и мгновенным откатом. Между этапами держим метрики — доля успешных вызовов и расхождение с эмуляцией. Если расхождений нет, версия раскатывается на всю систему; если есть — откат занимает один вызов, и пользователи его не замечают.
Частые технические сомнения перед стартом
Перед стартом заказчики задают один и тот же набор вопросов, и почти все они про границы: что именно проверяем, на какой сети, как это отражается на работающем продукте. Ниже — предметные ответы на те, что чаще всего тормозят решение.
Чем это отличается от проверки отдельных модулей?
Модульные проверки изолируют одну функцию. Интеграционный сценарий проходит путь целиком: вызов контракта, перевод активов, запись события, новое состояние. Тут всплывают неверные разрешения, порядок вызовов и повторный вход — то, что изоляция не видит.
Нужна ли реальная сеть?
Основной объём прогоняем на локальной копии состояния сети: окружение поднимается из конфигурации за минуты, один сценарий идёт секунды. Реальный контур нужен на финальном прогоне — он подтверждает поведение с настоящими адресами и лимитами.
Как понять, что покрытия достаточно?
Считаем не строки, а ветви сценариев: успешный путь, отказ по правам, отказ по лимитам, граничные суммы, откат. Если ветвь может остановить вывод средств — она покрыта тестом.
Как это влияет на работающий продукт?
Прогоны не касаются основной сети, рабочие ключи в них не используются. Редкие состояния — пустой резерв, лимит на исходе, зависшая транзакция — воспроизводим принудительно через снимок состояния, поэтому тесты идут параллельно с работой сервиса.
Обсудим задачу: с чего начнём и что нужно с вашей стороны
Первый созвон по интеграционному тестированию смарт-контрактов — технический, а не презентационный: разбираем архитектуру протокола, границы между контрактами и внешним миром и сценарии, где ошибка стоит дороже всего. Часа обычно хватает, чтобы понять объём — сколько контрактов в связке, какие из них обновляемые, что вызывается извне и какие роли вправе менять состояние.
Чтобы разговор был предметным, материалы стоит подготовить заранее. От заказчика нужны:
- репозиторий или архив контрактов вместе с существующими тестами, даже частичными;
- описание протокола: роли, права доступа, логика обновления контрактов;
- адреса развёрнутых контрактов и тестовых сетей, к которым есть рабочий доступ;
- перечень внешних интеграций — оракулы, мосты, пулы, кастодиальные сервисы — и их интерфейсы;
- ключи и узлы для тестовых сетей плюс сборочное окружение, если сценарии должны идти в конвейере;
- журнал инцидентов и известных дефектов: что уже ломалось и при каких условиях воспроизводилось;
- требования к приёмке — по каким сценариям считаем покрытие достаточным.
Разговор заканчивается не «спасибо, подумаем», а планом работ: этапы, состав локального стенда и копий основной сети, перечень сценариев с приоритетами, формат отчётов и оценка объёма — по срокам и загрузке команды. Старт проекта — сразу после согласования плана. Опишите задачу — вернёмся с планом и оценкой.







