Foundry setUp: исходное состояние теста
В предыдущем уроке появился полный CourseCatalog и договор его поведения. Чтобы исследовать этот договор, каждому сценарию нужно известное начало. Создадим фикстуру — набор объектов и данных, подготовленный перед отдельной проверкой. Она будет содержать новый реестр и два учебных адреса, но пока не будет публиковать курс.
Результатом урока станет файл test/CourseCatalog.t.sol с базовой подготовкой. Мы также разберём, почему нельзя рассчитывать, что другая проверка уже создала нужную запись. Показанные сценарии пока являются исходниками: Forge и контракт при подготовке текста не запускались.
Что означает независимое начало
Проверка публикации и проверка отказа читателю должны отвечать на разные вопросы. Если обе используют неизвестное состояние, результат становится неоднозначным. Например, getter вернул активную запись: это может быть результат текущего вызова, а может быть остаток действия, которое автор по ошибке считал частью общего начала.
В Foundry функция setUp() задаёт подготовку перед каждой проверкой. Разные test-функции получают исходную фикстуру, а не последовательную историю одного общего сеанса. Поэтому их названия и порядок в файле не должны служить скрытой программой приготовления данных. Мы будем явно выполнять нужную публикацию внутри сценария, который её исследует.
Полезно сравнить это с учебной задачей на бумаге. Перед каждым примером вы выдаёте ученику одинаковый чистый бланк и правила. Заполненный бланк одного ученика не становится условием задачи для следующего. Такое начало облегчает объяснение ошибки: известно, какие действия могли породить наблюдаемый результат.
Создаём базовый файл проверки
В файле приложения остаётся контракт прошлого урока. Теперь добавим полный начальный проверяющий файл. Он использует импорт закреплённой forge-std, наличие которой описано в профиле проекта.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
import {Test} from "forge-std/Test.sol";
import {CourseCatalog} from "../src/CourseCatalog.sol";
contract CourseCatalogTest is Test {
CourseCatalog internal catalog;
address internal publisher = address(0xA11CE);
address internal reader = address(0xB0B);
function setUp() public {
catalog = new CourseCatalog(publisher);
}
function testPublisherIsConfigured() public view {
assertEq(catalog.publisher(), publisher);
}
}
Наследование от Test предоставляет утверждения и доступ к учебному интерфейсу vm. Это не наследование самого реестра: CourseCatalog остаётся отдельным объектом. Переменная catalog хранит ссылку на него, поэтому вызов catalog.publisher() обращается к getter созданного экземпляра.
Адреса 0xA11CE и 0xB0B — разные значения для двух ролей. Они не являются сохранёнными приватными ключами и не требуют реальных кошельков. В первых сценариях мы назначаем отправителя специальным механизмом среды. Это помогает отделить правило «кто вправе вызвать функцию» от работы пользовательского расширения браузера.
setUp создаёт новый экземпляр с адресом издателя. Getter после этого должен возвращать именно переданное значение. Такая первая проверка нужна не для доказательства всей программы, а для явного подтверждения исходной роли. Если фикстура передаёт случайный адрес, следующие проверки запрета могут исследовать совсем другие условия.
Общее начало должно быть небольшим
Можно было сразу публиковать номер 17 внутри setUp. Тогда у каждого сценария возникла бы готовая активная запись. Это удобно для исследования деактивации, но неудобно для проверки отсутствующего курса и самой первой публикации. Поэтому исходная фикстура остаётся пустой, а действие добавляется там, где оно требуется.
С ростом курса появятся helper-функции для повторяющейся подготовки. Они должны ясно называться: например, _publishCourse объясняет добавляемое состояние лучше, чем _prepareEverything. Вызов такого помощника внутри сценария показывает, что запись возникает в этой проверке, а не является неочевидным побочным эффектом общего начала.
| Часть фикстуры | Где задаётся | Причина |
|---|---|---|
| Адрес издателя | Поле проверяющего контракта | Одинаковая роль для всех сценариев |
| Адрес читателя | Поле проверяющего контракта | Известная чужая роль |
| Новый реестр | setUp |
Независимое исходное состояние |
| Выпуск курса 17 | В конкретном сценарии | Нужен не каждой проверке |
| Отправитель действия | Перед соответствующим вызовом | Относится к операции, а не к одному имени переменной |
Эта таблица показывает границу общего и частного. Подготовка не должна незаметно выбирать исход, который как раз собирается проверять сценарий. Если тест исследует создание первой записи, наличие заранее созданной записи лишает его первоначального смысла.
Две проверки не образуют последовательность
Представим две функции: одна публикует выпуск, вторая читает его и требует active == true. Если во второй отсутствует собственная публикация, её ожидание не следует из пустой фикстуры. Порядок имён test01... и test02... не исправляет модель. Правильное решение — либо собрать оба действия в один связный сценарий, либо подготовить необходимую запись отдельно во второй проверке.
Для деактивации это будет выглядеть так: создать исходный реестр, выполнить разрешённую публикацию, затем деактивировать и сравнить поля. Все причины результата находятся в одной функции или явно вызываемых помощниках. Читатель сможет воспроизвести ход рассуждения, не просматривая предыдущие результаты другого запуска.
Изоляция не означает, что в одной проверке можно игнорировать последствия предыдущего вызова. Наоборот, сценарий обновления обязан сохранять свою последовательность действий. Различаем изоляцию между сценариями и переходы внутри одного сценария. Первое убирает случайные зависимости, второе позволяет исследовать сложное поведение.
Как наблюдать ожидаемое начало
После setUp ожидается настроенный издатель и пустые записи. Пока мы проверили в исходнике только издателя. В следующем уроке добавим утверждения для отсутствующего номера и разберём форму возвращаемых значений. Это будет отдельный результат, потому что неправильное чтение структуры может создать ложное представление о состоянии.
Не следует записывать в README конкретный адрес catalog как адрес будущей публичной системы. Экземпляр создаётся в учебной среде и относится к отдельному сценарию. Адрес действующего контракта в выбранной сети — другой факт, для которого нужен собственный процесс развёртывания и запись происхождения.
Также не будем заранее сохранять строку «1 passed» как полученный ответ. У нас есть функция с ясным ожиданием, но она пока не исполнена. Когда появится разрешённый этап запуска, результат можно будет сопоставить с тем же профилем и исходниками. Такая запись делает ограничения материала понятными следующему разработчику.
Сохраните базовый файл без публикации в setUp. В следующем уроке сравним точный отпечаток и булев флаг отсутствующего курса, затем увидим, почему удобное общее условие не всегда проверяет нужное свойство.
Соглашения проверяющих файлов и setUp описаны в руководстве Forge. Все этапы лаборатории находятся в оглавлении курса.