TON Blockchain Сourse #FREEDUROV
737 subscribers
17 photos
73 videos
30 files
127 links
для связи @Aleksey_Leon

#freedurov
Download Telegram
Готовим наш набор тестов
Прежде всего, предполагая, что вы в данный момент находитесь в корне нашего проекта, давайте установим sandbox, jest и еще одну библиотеку, которая нам понадобится для взаимодействия с TON объектами - ton:

yarn add @ton-community/sandbox jest ts-jest @types/jest ton --dev

Нам также нужно будет создать jest.config.js файл в корне нашего проекта для just со следующим содержимым:

module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};


Теперь давайте создадим новую папку test с файлом main.spec.ts:

mkdir tests && cd tests && touch main.spec.ts

Давайте настроим наш основной файл.spec.ts для написания наших первых тестов:

describe("main.fc contract tests", () => {

it("our first test", async () => {

});

});


Если вы никогда не писали тесты на TypeScript, вам обязательно стоит попробовать этот подход. При программировании НА смарт-контрактах вы потратите более половины своего времени на написание тестов. Помните наш пример с космическим спутником? То же самое и здесь, мы моделируем каждый важный случай, прежде чем развертывать контракт даже для тестирования net.

Теперь попробуйте запустить команду yarn jest в корне вашего каталога. Если вы все установили правильно (шаг за шагом со мной) - вы должны увидеть следующее:

PASS tests/main.spec.ts
main.fc contract tests
✓ our first test (1 ms)


Отлично, давайте немедленно создадим еще один ярлык для запуска скрипта в нашем файле package.json:

{
... our previous package.json keys
"scripts": {
... previous scripts keys
"test": "yarn jest"
}
}
Создание экземпляра контракта
Чтобы написать наш первый тест, нам нужно понять, как мы можем создать экземпляр TypeScript нашего скомпилированного контракта с помощью sandbox.

Ранее мы обсуждали, что код нашего контракта сохраняется в виде ячейки после компиляции. В нашем файле build/main.compiled.json у нас есть шестнадцатеричное представление нашей ячейки. Давайте импортируем его в наш файл с тестами вместе с типом ячейки из to-core:

import { Cell } from "ton-core";
import { hex } from "../build/main.compiled.json";

describe("main.fc contract tests", () => {

it("our first test", async () => {

});

});


Теперь, чтобы восстановить наш шестнадцатеричный код и получить реальную ячейку, мы воспользуемся этой командой:

const codeCell = Cell.fromBoc(Buffer.from(hex, "hex"))[0]

Мы создаем буфер из шестнадцатеричной строки и передаем его в .fromBocc.

Теперь давайте взглянем на краткое руководство по запуску sandbox. Мы видим, что перед получением экземпляра контракта мы должны получить экземпляр блокчейна.

Давайте импортируем класс Blockchain из библиотеки sandbox и вызовем его метод .create().

import { Cell } from "ton-core";
import { hex } from "../build/main.compiled.json";
import { Blockchain } from "@ton-community/sandbox";

describe("main.fc contract tests", () => {
it("our first test", async () => {
const blockchain = await Blockchain.create();

});
});


Теперь давайте подготовимся к созданию экземпляра contract для взаимодействия. В документации Sandbox указано, что рекомендуемый способ его использования - написать оболочки для вашего контракта, используя интерфейс Contract из ton-core

Что бы это значило для нас? Давайте создадим новую папку wrappers и файл с именем Main Contract.ts внутри нее. Этот файл будет реализовывать и экспортировать оболочку вокруг нашего контракта.

mkdir wrappers && cd wrappers && touch MainContract.ts

Убедитесь, что вы запустили эту последовательность команд из корневого каталога вашего проекта

Откройте MainContract.ts для редактирования. Давайте импортируем интерфейс Contract из библиотеки to-core, затем определим и экспортируем класс, который будет реализовывать Contract.

import { Contract } from 'ton-core';

export class MainContract implements Contract {

}


Если вы взглянете на содержимое интерфейса Contract - вы увидите, что он ожидает параметры address, init и abi. Мы будем использовать только address и init для наших целей. Чтобы использовать их, мы определяем конструктор для нашего класса MainContract

Если вы понятия не имеете, о чем мы здесь говорим с классами и конструкторами - возможно, вам стоит почитать больше об объектно-ориентированном программировании. FunC этого не требует, но для лучшего тестирования с помощью TypeScript вам не следует использовать эти базовые понятия.

import { Address, Cell, Contract } from "ton-core";

export class MainContract implements Contract {
constructor(
readonly address: Address,
readonly init?: { code: Cell; data: Cell }
) {}
}


Свойство init очень интересно. В нем указано начальное состояние нашего контракта. Code - это, очевидно, код контракта. Данные - это еще интереснее. Помните, мы говорили о постоянном хранилище c4 нашего контракта? С помощью этой ячейки данных мы можем определить, что будет находиться в этом хранилище после первого выполнения нашего контракта. Оба этих значения Cell имеют тип ячейки, поскольку они хранятся в памяти TVM в течение жизненного цикла контракта. Тот же код и ячейки данных также используются для расчета будущего адреса нашего контракта.

Обратите внимание, что мы также импортировали классы Address и Cell из библиотеки to-core.

Теперь давайте определим статический метод для нашего MainContract класса - createFromConfig. Сейчас мы не собираемся использовать какие-либо параметры конфигурации, но в будущем мы предполагаем, что нам понадобятся входные данные для создания экземпляра контракта.
import { Address, beginCell, Cell, Contract, contractAddress } from "ton-core";

export class MainContract implements Contract {
constructor(
readonly address: Address,
readonly init?: { code: Cell; data: Cell }
) {}

static createFromConfig(config: any, code: Cell, workchain = 0) {
const data = beginCell().endCell();
const init = { code, data };
const address = contractAddress(workchain, init);

return new MainContract(address, init);
}
}


Наш метод createFromConfig принимает параметр config (пока игнорируйте), codeкоторый представляет собой ячейку со скомпилированным кодом нашего контракта и workchain, которая определяет рабочую цепочку TON, в которую должен быть помещен контракт. В настоящее время существует только одна рабочая цепочка на ton - 0.

Чтобы лучше понять этот код, давайте начнем с результатов, которые мы возвращаем. Мы создаем новый экземпляр нашего класса MainContract, и, как определено в его конструкторе, мы должны передать ему будущий address контракта (который мы можем вычислить, как отмечалось ранее) и init начальное состояние контракта.

Адрес, который мы вычисляем с помощью функции, которую мы импортируем из библиотеки ton-core. Мы передаем параметры состояния workchain и init, чтобы получить его.

Состояние init - это просто объект со свойствами code и data. Место, где передается code, передается в наш метод, а data пока представляют собой просто пустую ячейку. Мы узнаем, как преобразовать config данные в формат ячейки, чтобы мы могли использовать config данные в состоянии init нашего контракта.

Подводя итог, createFromConfig принимает config с данными, которые мы в будущем сохраним в постоянном хранилище контракта, и code контракта. Взамен мы получаем экземпляр контракта, с которым мы можем легко взаимодействовать с помощью sandbox.

Давайте вернемся к нашим tests/main.spec.ts и выполним следующие шаги:

- импортируем нашу оболочку
- импортируем скомпилированный код контракта в виде шестнадцатеричного представления
- преобразуем его в ячейку
- используем openContract sandbox вместе с нашей новой оболочкой, чтобы получить экземпляр контракта, с которым мы, наконец, сможем взаимодействовать

import { Cell } from "ton-core";
import { hex } from "../build/main.compiled.json";
import { Blockchain } from "@ton-community/sandbox";
import { MainContract } from "../wrappers/MainContract";

describe("main.fc contract tests", () => {
it("our first test", async () => {
const blockchain = await Blockchain.create();
const codeCell = Cell.fromBoc(Buffer.from(hex, "hex"))[0];

const myContract = blockchain.openContract(
await MainContract.createFromConfig({}, codeCell)
);
});
});


На данный момент у нас есть экземпляр смарт-контракта, с которым мы можем взаимодействовать во многом аналогично реальному контракту, чтобы протестировать ожидаемое поведение.
Взаимодействие с контрактом
Библиотека ton-core предоставляет нам еще одну замечательную конструкцию под названием Address . Как вы знаете, у каждого объекта в блокчейне TON есть адрес. В реальной жизни, если вы хотите отправить сообщение из одного контракта (например, контракта кошелька) в другой, вы знаете адреса обоих контрактов.

При написании тестов мы эмулируем контракт, а при взаимодействии — известен адрес нашего эмулируемого контракта. Однако нам понадобится кошелек, который мы будем использовать для развертывания нашего контракта, и еще один для взаимодействия с нашим контрактом.

Это sandbox делается таким образом, что мы вызываем treasure метод экземпляра блокчейна и предоставляем ему начальную фразу: const senderWallet = await blockchain.treasury("sender");

Эмуляция внутреннего сообщения
Вернемся к написанию нашего теста. Поскольку у нас есть экземпляр нашего контракта, давайте отправим ему внутреннее сообщение, чтобы наш адрес отправителя был сохранен в хранилище c4. Мы помним, что для взаимодействия с нашим экземпляром контракта нам нужно использовать обертку. Давайте вернемся к нашим файлам- wrappers/MainContract.ts и создадим в нашей оболочке новый метод под названием sendInternalMessage .

Изначально это будет выглядеть так:

async sendInternalMessage(
provider: ContractProvider,
sender: Sender,
value: bigint
){

}

Наш новый метод получает параметр типа ContractProvider , отправителя сообщения типа Sender и значение сообщения value .

Обычно нам не нужно беспокоиться о передаче ContractProvider при использовании этого метода, он будет передаваться «под капотом» как часть экземпляра контракта, встроенного в функциональные возможности. Однако не забудьте импортировать эти типы из библиотеки ton-core .

Давайте реализуем логику отправки внутреннего сообщения внутри нашего нового метода. Это будет выглядеть так:

async sendInternalMessage(
provider: ContractProvider,
sender: Sender,
value: bigint
) {
await provider.internal(sender, {
value,
sendMode: SendMode.PAY_GAS_SEPARATELY,
body: beginCell().endCell(),
});
}

Вы можете видеть, что мы используем provider и вызываем его метод под названием Internal . Мы передаем отправителя sender в качестве первого параметра, затем формируем объект с аргументами, который включает в себя:

- value сообщения (количество TON в формате nano)
- sendMode — мы используем перечисление SendMode, предоставленное ton-core, вы можете узнать больше о том, как эти режимы работают «под капотом», на странице документации , посвященной этому.
- body , которое должно быть ячейкой с телом сообщения, но мы пока оставляем его пустой ячейкой
Пожалуйста, не забудьте импортировать все новые типы и объекты, которые мы используем, из ton-coreбиблиотеки.

Итак, наш окончательный код wrappers/main выглядит так:

import { Address, beginCell, Cell, Contract, contractAddress, ContractProvider, Sender, SendMode } from "ton-core";

export class MainContract implements Contract {
constructor(
readonly address: Address,
readonly init?: { code: Cell; data: Cell }
) {}

static createFromConfig(config: any, code: Cell, workchain = 0) {
const data = beginCell().endCell();
const init = { code, data };
const address = contractAddress(workchain, init);

return new MainContract(address, init);
}

async sendInternalMessage(
provider: ContractProvider,
sender: Sender,
value: bigint
) {
await provider.internal(sender, {
value,
sendMode: SendMode.PAY_GAS_SEPARATELY,
body: beginCell().endCell(),
});
}

}

Вызов этого метода в нашем 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 и в результате возвращаем его.

Написание тестов

Вот и все. Мы выполнили всю подготовительную работу и теперь напишем собственно тестовую логику. Вот как будет выглядеть наш тестовый сценарий:

1. Отправляем и внутреннее сообщение
2. Мы гарантируем, что отправка прошла успешно
3. Мы вызываем метод получения контракта и проверяем, что вызов прошел успешно.
4. Мы сравниваем результаты, полученные от метода получения, с адресом from , который мы установили в исходном внутреннем сообщении.

Кажется довольно простым и выполнимым! Давай сделаем это.

Команда Sandbox предоставила нам еще одну замечательную утилиту для тестирования. Мы можем установить дополнительный пакет @ton-community/test-utils, запустив yarn add @ton-community/test-utils -D

Это позволит нам использовать .toHaveTransaction для средства сопоставления jest, чтобы добавить дополнительных помощников для упрощения тестирования. Нам также нужно будет импортировать этот пакет в наш test/main.spec.ts после установки.

Давайте посмотрим, как выглядит наш тестовый код, основанный на описанном выше сценарии.

import { Cell, toNano } from "ton-core";
import { hex } from "../build/main.compiled.json";
import { Blockchain } from "@ton-community/sandbox";
import { MainContract } from "../wrappers/MainContract";
import "@ton-community/test-utils";

describe("main.fc contract tests", () => {
it("should get the proper most recent sender address", async () => {
const blockchain = await Blockchain.create();
const codeCell = Cell.fromBoc(Buffer.from(hex, "hex"))[0];

const myContract = blockchain.openContract(
await MainContract.createFromConfig({}, codeCell)
);

const senderWallet = await blockchain.treasury("sender");

const sentMessageResult = await myContract.sendInternalMessage(
senderWallet.getSender(),
toNano("0.05")
);

expect(sentMessageResult.transactions).toHaveTransaction({
from: senderWallet.address,
to: myContract.address,
success: true,
});

const data = await myContract.getData();

expect(data.recent_sender.toString()).toBe(senderWallet.address.toString());
});
});


Мы используем функциональность Jest, чтобы убедиться, что:

- отправка внутреннего сообщения прошла успешно
- метод получения прошел успешно
- метод получения возвращает некоторый результат
- адрес, возвращаемый методом получения, равен тому, который мы использовали в нашем сообщении, устанавливая адрес отправителя

Мы также переименовали «наш первый тест» в «должен получить правильный последний адрес отправителя», мы хотим, чтобы имена наших тестов всегда были читабельными.

Запуск тестов

Вуаля! Мы готовы провести наши тесты. Просто запустите нашу команду yarn testв терминале, и если вы все сделали вместе со мной, вы получите аналогичный результат:

PASS tests/main.spec.ts
main.fc contract tests
✓ should get the proper most recent sender address (444 ms)

Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 4.13 s, estimated 5 s


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

Обновите файл package.json , чтобы он выглядел следующим образом:
{
... our previous package.json keys
"scripts": {
... previous scripts keys
"test": "yarn compile && yarn jest"
}
}

На следующем уроке мы собираемся построить наш конвейер развертывания и узнать, как протестировать реальный контракт развертывания в сети.
Media is too big
VIEW IN TELEGRAM
3.5 Развертывание смарт-контракта
✔️ Так же смотрите видео на YouTube
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥1👨‍💻1
3.5 Развертывание смарт-контракта.pdf
490.4 KB
📚Конспекты лекций на русском
3.5 Развертывание смарт-контракта
👎3👍1
Media is too big
VIEW IN TELEGRAM
3.6 Гибкое развертывание из тестовой сети в основную
✔️ Так же смотрите видео на YouTube
Please open Telegram to view this post
VIEW IN TELEGRAM
👨‍💻11
3_6_Гибкое_развертывание_из_тестовой_сети_в_основную.pdf
155.4 KB
📚Конспекты лекций на Русском
3.6 Гибкое развертывание из тестовой сети в основную
3_4_Рабочий_процесс_тестирования_и_написание_тестов.pdf
456.5 KB
📚Конспекты лекций
3.4 Рабочий процесс тестирования и написание тестов
3_3_Написание_простого_функционального_контракта.pdf
218.2 KB
📚Конспекты лекций
3.3 Написание простого функционального контракта
3_2_Настройка_рабочего_процесса_компиляции.pdf
270.6 KB
📚Конспекты лекций
3.2 Настройка рабочего процесса компиляции
3_1_Обзор_жизненного_цикла_разработки_смарт_контрактов.pdf
213 KB
📚Конспекты лекций
3.1 Обзор жизненного цикла разработки смарт-контрактов
4.1 контракт со встречной логикой_.pdf
175.4 KB
❗️Конспект лекции на Русском
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
4_2_тесты_для_встречного_контракта.pdf
211.1 KB
❗️Конспект лекции на Русском
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Please open Telegram to view this post
VIEW IN TELEGRAM
👎2
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Please open Telegram to view this post
VIEW IN TELEGRAM
👎1
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Media is too big
VIEW IN TELEGRAM
4.1 Контракт со встречной логикой
✔️ Так же смотрите видео на YouTube
Please open Telegram to view this post
VIEW IN TELEGRAM
2👨‍💻1