📚Чтение заметок
Мы написали наш первый контракт FunC, мы знаем, что он успешно компилируется. Что дальше?
В этом уроке мы узнаем, как убедиться, что код нашего контракта действительно работает так, как ожидалось. Позвольте мне напомнить вам о наших ожиданиях от кода:
Наш контракт должен сохранять адрес отправителя каждый раз, когда он получает сообщение, и он должен возвращать последний адрес отправителя, как только мы вызываем метод получения.
Отлично, но как мы можем убедиться, что это работает? Ну, мы могли бы развернуть его в блокчейне (что мы в конечном итоге сделаем очень скоро, в следующем уроке), но у TON есть некоторые инструменты, которые позволяют нам моделировать определенное поведение локально.
Это делается с помощью песочницы. Это library Позволяет эмулировать произвольные смарт-контракты TON, отправлять им сообщения и запускать методы get, как если бы они были развернуты в реальной сети. Поскольку у нас есть "лаборатория" TypeScript, мы можем создать последовательность тестов с помощью другой библиотеки - jest. Таким образом, у нас есть набор тестов, который моделирует все важные действия, с различными входными данными и проверяет результаты. Это лучший подход к написанию, отладке и полному тестированию ваших контрактов перед их запуском в сеть.
Подготовка нашего тестового набора
Прежде всего, если предположить, что вы в настоящее время находитесь в корне нашего проекта, давайте установим sandbox, jest и еще одну библиотеку, которая нам понадобится для взаимодействия с сущностями TON - ton:
Creating contract instance
Чтобы написать наш первый тест, нам нужно понять, как мы можем создать экземпляр TypeScript нашего скомпилированного контракта с помощью sandbox.
Мы уже обсуждали, что наш контрактный код сохраняется как ячейка после компиляции. В нашем файле build/main.compiled.json есть шестнадцамадехнадцевое представление нашей ячейки. Давайте импортируем его в наш файл с тестами вместе с типом ячейки из ton-core:
Теперь, чтобы восстановить наш шестнадеряный и получить настоящую ячейку, мы будем использовать эту команду: const codeCell = Cell.fromBoc(Buffer.from(hex, "hex"))[0].
We create a Buffer from a hexadecimal string and pass it to the .fromBoc method.
Now let's take a look at sandbox quick start guide. We see that before getting an instance of a contract, we have to get an instance of blockchain.
Let's import a class Blockchain from sandbox library and call it's .create() method.
Теперь давайте подготовимся к тому, чтобы получить экземпляр контракта для взаимодействия. В документации Sandbox говорится, что рекомендуемый способ его использования - написать обертки для вашего контракта с помощью интерфейса контракта из ton-core
Что бы это значило для нас? Давайте создадим в них новые обертки папок и файл под названием MainContract.ts. Этот файл будет реализовывать и экспортировать обертку вокруг нашего контракта.
Mkdir wrappers && cd wrappers && touch MainContract.ts
Убедитесь, что вы запустите эту последовательность команд из корня вашего проекта
Откройте MainContract.ts для редактирования. Давайте импортируем интерфейс контракта из библиотеки ton-core, а затем определим и экспортируем класс, который будет реализовывать контракт.
Собственность init очень интересна. У него есть первоначальное состояние нашего контракта. Код, очевидно, является кодом контракта. Данные немного интереснее. Помните, мы говорили о постоянном хранении c4 нашего контракта? С помощью этой ячейки данных мы можем определить, что будет в этом хранилище после первого выполнения нашего контракта. Оба этих значения имеют тип ячейки, так как они хранятся в памяти TVM в течение жизненного цикла контракта. Тот же код и ячейки данных также используются для расчета будущего адреса нашего контракта.
Обратите внимание, что мы также импортировали классы Address и Cell из библиотеки @ton/core.
В TON адреса контрактов детерминичны. Мы можем узнать их еще до того, как развернем наш контракт. Мы увидим, как это будет сделано за несколько шагов.
Теперь давайте определим статический метод для нашего класса MainContract - createFromConfig. На данный момент мы не собираемся использовать какие-либо параметры конфигурации, но в будущем мы предполагаем, что нам понадобятся входные данные для создания экземпляра контракта.
Наш метод createFromConfig принимает параметр конфигурации (на данный момент игнорировать), код, который представляет собой ячейку с скомпилированным кодом нашего контракта, и рабочую цепочку, которая определяет рабочую цепочку TON, на которой должен быть размещен контракт. В настоящее время есть только одна рабочая цепь на тонне - 0.
Чтобы лучше понять этот код, давайте начнем с результатов, которые мы возвращаем. Мы создаем новый экземпляр нашего класса MainContract, и, как определено в его конструкторе, мы должны передать его будущий адрес контракта (который мы можем рассчитать, как было отмечено ранее) и состояние начала контракта.
Адрес, который мы вычисляем по функции, которую мы импортируем из библиотеки @ton/core. Мы передаем параметры рабочей цепочки и состояния инита, чтобы получить его.
Исходное состояние является простым и объектом со свойствами кода и данных. Место передачи кода передается в наш метод, и на данный момент данные являются просто пустой ячейкой. Мы узнаем, как преобразовать данные конфигурации в формат ячейки, чтобы мы могли использовать данные конфигурации в состоянии it нашего контракта.
Подводя итог, createFromConfig принимает конфигурацию с данными, которые мы будем в будущем хранить в постоянном хранилище контракта и коде контракта. Взамен мы получаем экземпляр контракта, с которым мы можем легко взаимодействовать с помощью песочницы.
Давайте вернемся к нашим tests/main.spec.ts и выполним следующие действия:
- Импортируйте нашу обертку
- Импортируйте скомпилированный код контракта в качестве шестнадестнадерного представления
- Превратить его в клетку
- Используйте openContract песочницы вместе с нашей новой оберткой, чтобы получить экземпляр контракта, с которым мы, наконец, можем взаимодействовать
На данный момент у нас есть пример смарт-контракта, с которым мы можем взаимодействовать во многих отношениях, подобно реальному контракту, чтобы проверить ожидаемое поведение.
Взаимодействие с контрактом
Библиотека @ton/core предоставляет нам еще одну отличную конструкцию под названием Address. Как вы знаете, каждая организация в блокчейне TON имеет адрес. В реальной жизни, если вы хотите отправить сообщение с одного контракта (например, с кошельком) на другой - вы знаете адреса обоих контрактов.
При написании тестов мы эмулируем контракт, и когда мы взаимодействуем с ним - адрес нашего эмулируемого контракта известен. Тем не менее, нам понадобится кошелек, который мы будем использовать для развертывания нашего контракта, и еще один для взаимодействия с нашим контрактом.
В песочнице это делается таким образом, что мы называем метод сокровища экземпляра блокчейна и предоставляем ему начальную фразу: const senderWallet = await blockchain.treasury("sender");
Эмулирование внутреннего сообщения
Давайте вернемся к написанию нашего теста. Поскольку у нас есть экземпляр нашего контракта, давайте отправим ему внутреннее сообщение, чтобы наш адрес отправителя был сохранен в хранилище c4. Мы помним, что для взаимодействия с нашим экземпляром контракта нам нужно использовать обертку. Давайте вернемся к нашим оберткам файлов/MainContract.ts и создадим новый метод в нашей обертке под названием sendInternalMessage.
Изначально это выглядело бы так:
async sendInternalMessage(
provider: ContractProvider,
sender: Sender,
value: bigint
){
}
Наш новый метод получает параметр типа ContractProvider, отправитель сообщения типа Sender и значение значения сообщения.
Обычно нам не нужно беспокоиться о передаче ContractProvider при использовании этого метода, он будет передан под капотом как часть встроенного экземпляра контракта. Тем не менее, не забудьте импортировать эти типы из основной библиотеки.
Давайте реализуем логику отправки внутреннего сообщения внутри нашего нового метода. Это будет выглядеть так:
Вы можете видеть, что мы используем провайдера и называем его метод внутренним. Мы передаем отправителя в качестве первого параметра, затем формируем объект с аргументами, которые включают в себя:
- value Сообщения (количество тон в формате нано)
- sendMode -Мы используем enum SendMode, предоставленный @ton/core, вы можете узнать больше о том, как режимы работают под капотом в documentation page Посвященный этому
- body - Это должна быть ячейка с телом сообщения, но пока мы оставим ее пустой ячейкой
Пожалуйста, не забудьте импортировать все новые типы и сущности, которые мы используем, из библиотеки @ton/core.
Итак, наш окончательный код wrappers/MainContract.ts выглядит следующим образом:
Вызов этого метода в наших tests/main.spec.ts будет выглядеть следующим образом:
const senderWallet = await blockchain.treasury("sender"); myContract.sendInternalMessage(senderWallet.getSender(), toNano("0.05"));
Обратите внимание, как мы используем функцию помощи toNano, импортированную из библиотеки @ton/core, для преобразования строкового значения в формат nano bignum.
Вывод метода геттера
Нам нужно будет создать еще один метод на обертке нашего контракта, а именно - метод getData, который будет запустить метод геттера нашего контракта и вернуть результаты, которые у нас есть в хранилище c4.
Вот как будет выглядеть наш метод геттера:
async getData(provider: ContractProvider) {
const { stack } = await provider.get("get_the_latest_sender", []);
return {
recent_sender: stack.readAddress(),
};
}
Как и в случае с отправкой внутреннего сообщения - мы используем провайдера и его методы. В этом случае мы используем get Метод. Затем мы читаем адрес от полученного stack И верните его в результате.
Написание тестов
Вот и все. Мы проделали всю подготовительную работу, и теперь мы напишем фактическую логику теста. Вот как будет выглядеть наш тестовый сценарий:
- Мы отправляем и внутреннее сообщение
- Мы гарантируем, что отправка прошла успешно
- Мы вызываем метод геттера контракта и гарантируем, что звонок был успешным.
- Мы сравниваем результаты, полученные от getter, с адресом from, который мы установили в исходном внутреннем сообщении.
Кажется довольно простым и выполнимым! Давай сделаем это.
Команда Sandbox оснастила нас еще одним тестовым утилитой amasing. Мы можем установить дополнительный пакет @ton/test-utils, заполнив yarn add @ton/test-utils -D
Это позволит нам использовать .toHaveTransaction для jest matcher, чтобы добавить дополнительных помощников для удобства тестирования. Нам также нужно будет импортировать этот пакет в наши tests/main.spec.ts после установки.
Давайте посмотрим, как выглядит наш код тестирования, основанный на описанном выше сценарии.
Мы используем функцию Jest, чтобы убедиться, что:
- Внутренняя отправка сообщения прошла успешно
- Метод геттера был успешным
- Метод геттера возвращает некоторый результат
- Адрес, возвращенный из метода getter, равен тому, который мы использовали в нашем сообщении, устанавливая адрес from
Мы также переименовали «наш первый тест» на «должен получить правильный последний адрес отправителя», мы хотим, чтобы имена наших тестов всегда были читаемыми.
Запуск тестов
Вуаля! Мы готовы провести наши тесты. Просто запустите наш командный тест пряжи в терминале, и если вы все сделали со мной, вы получите аналогичный результат:
Последнее, что мы хотим сделать, это убедиться, что каждый раз, когда мы запускаем тесты, мы также запускаем наш скрипт компилятора. Это полезно для эффективности нашей работы. Большая часть работы по разработке заключается в написании функционального кода, а затем в запуске тестов. Давайте упорядочим это:
Обновите файл package.json, чтобы он выглядел следующим образом: