Перейти к содержанию

Foundry: assertions и проверка пустой записи

В прошлом уроке мы создали фикстуру с новым CourseCatalog и известным издателем. Теперь исследуем её пустое состояние. Getter должен возвращать определённые значения для номера, который никто не публиковал. Напишем утверждения отдельно для отпечатка и флага, чтобы не скрыть одну ошибку за другой.

Результат этого урока — точная проверка отсутствующего курса и понимание того, какую часть договора подтверждает каждое утверждение. Мы по-прежнему готовим исходники без исполнения. Таблицы ниже описывают ожидаемое состояние, а не извлечённый ответ запущенной лаборатории.

Читаем структуру через getter

Публичный mapping(uint256 => Course) предоставляет функцию courses(uint256). Для структуры Course она возвращает два значения: bytes32 contentHash и bool active. Поэтому переменной нужно принять не один «объект курса», а результат с соответствующими типами. В Solidity это удобно выразить присваиванием кортежа.

Добавим следующую функцию внутрь уже существующего CourseCatalogTest. Импорты, поле catalog и setUp остаются из прошлого урока; новый файл приложения создавать не нужно.

function testMissingCourseHasDefaultValues() public view {
    (bytes32 digest, bool active) = catalog.courses(17);

    assertEq(digest, bytes32(0));
    assertFalse(active);
}

Номер 17 выбран как наш постоянный пример. В фикстуре не было публикаций, поэтому для этого ключа ожидаются стандартные значения структуры. Для bytes32 это 32 нулевых байта, для bool — false. Утверждение assertEq сравнивает конкретные байты, а assertFalse проверяет отдельный флаг.

Нулевая запись не является исключением getter. Она возникает из устройства mapping: каждому ключу соответствует значение, а не созданная вручную строка с признаком существования. Поэтому утверждение «чтение отсутствующего курса обязательно откатывается» противоречит нашей реализации и договору. Проверка должна учитывать выбранную модель данных.

Почему одного флага недостаточно

Допустим, по ошибке реализация записала ненулевой отпечаток в неактивный курс. Условие assertFalse(active) всё равно выполняется. Если требование к пустой фикстуре включает нулевой отпечаток, его нужно проверить отдельно. Иначе сценарий подтверждает только половину ожидаемого состояния.

Обратная ситуация — отпечаток нулевой, но флаг ошибочно установлен. Сравнение байтов само по себе не обнаружит этот дефект. Две независимые проверки ясно объясняют, какая часть записи расходится с договором. Это особенно полезно, когда структура позднее получит дополнительные поля.

Отпечаток Флаг Для пустой фикстуры
Нулевой false Ожидаемое состояние
Ненулевой false Не соответствует исходной записи
Нулевой true Не соответствует исходной записи
Ненулевой true Похоже на публикацию, которой не было

Таблица относится именно к пустой фикстуре. Для деактивированного опубликованного курса сочетание «ненулевой, false» будет правильным. Следовательно, нельзя вынести это сочетание в универсальную ошибку без учёта истории сценария. Один набор значений получает смысл из известного начального состояния и выполненных действий.

Сравниваем значение того же типа

При работе с байтами легко смешать разные представления. bytes32(0) — значение фиксированной длины. Строка "0" содержит символ, а строка "0x00" содержит несколько символов текстового описания. Их хеши тоже не превращаются в нулевой отпечаток. Поэтому ожидание записывается в том же типе, который возвращает getter.

В этом уроке нет преобразования ответа в JSON или пользовательское отображение. Мы исследуем Solidity-интерфейс непосредственно. Форматирование шестнадцатеричного отпечатка на странице относится к другой границе. Если браузер обрезал строку для красоты, это не меняет действительное значение, которое должна сравнивать проверка контракта.

Аналогично номер курса является uint256. Значение 17 здесь не обозначает сумму сатоши, число подтверждений или позицию в массиве. Имя переменной и договор помогают сохранить его назначение. Для больших проектов полезно разделять такие смыслы даже тогда, когда их базовый машинный тип совпадает.

Утверждение должно соответствовать вопросу

assertTrue, assertFalse и assertEq — способы описать разные ожидания. Если нужно сравнить конкретный отпечаток, условие «он просто ненулевой» слишком слабое. Неверный ненулевой отпечаток пройдёт такую проверку. Если нужно проверить флаг, нет необходимости превращать его в число и сравнивать с произвольным кодом состояния.

Позднее для разрешённых диапазонов мы используем утверждения сравнения, например «не меньше нижней границы». Но применять их к номеру курса здесь было бы сменой задачи. Getter должен вернуть определённую запись, а не приблизительно похожую величину. Выбирайте минимальное условие, которое действительно выражает необходимое свойство, не ослабляя его ради удобного прохождения.

Порядок утверждений влияет на разбор отказа. Если первое завершило сценарий ошибкой, второе ещё не даёт наблюдаемого результата этого исполнения. Поэтому из одного сообщения нельзя заключить, что все остальные поля правильные. При дальнейшей диагностике потребуется исправить первую причину или разделить вопросы на отдельные сценарии.

Изоляция записи другого номера

Какое дополнительное свойство можно исследовать без изменения базовой модели? Публикация номера 17 не должна автоматически создать номер 18. Пока мы ещё не научились задавать отправителя публикации, поэтому оставим этот вариант для последующих уроков. Сам вопрос полезен уже сейчас: важно различать состояние одного ключа и состояние всего реестра.

Для простого getter два одинаковых чтения подряд должны давать один результат, если между ними ничего не изменилось. Но такой повтор не заменяет проверку правильности. Ошибочная постоянная реализация тоже может стабильно возвращать неправильное значение. Воспроизводимость ответа и соответствие требованиям — разные свойства.

В следующем шаге появится разрешённая публикация. Мы тогда проверим точный переданный отпечаток, активный флаг и сохранение пустой записи другого номера. Это продолжит один и тот же реестр, а не создаст новый пример с другой семантикой отсутствия.

Граница текущего результата

Сохранённая функция описывает два утверждения о пустой фикстуре. Она не доказывает запрет записи читателю, корректность события или все возможные переходы. Хороший узкий сценарий имеет понятную область: мы знаем, что он способен заметить и чего ещё не исследует.

При будущей проверке следует сопоставить реальный вывод с этим договором и закреплённым профилем. Пока не нужно добавлять выдуманную статистику успешного прогона. Мы получили исходник ожидания, а не сертификат качества приложения.

В следующем уроке назначим отправителя конкретного вызова. Это позволит отличить действия издателя и читателя, сохранив те же номера и структуру реестра. Соглашения утверждений можно сверять с forge-std, а модель mapping — с документацией Solidity. Оглавление курса.

Оглавление серии