Примеры написания тестов
Эта страница переведена сообществом на русский язык, но нуждается в улучшениях. Если вы хотите принять участие в переводе свяжитесь с @alexgton.
На этой странице показано, как писать тесты для контрактов FunC, созданных в Blueprint SDK (Sandbox).
Наборы тестов созданы для демо-контракта fireworks. Fireworks - это смарт-контракт, который изначально запускается через сообщение set_first
.
После создания нового проекта FunC с помощью npm create ton@latest
, в директории проекта будет автоматически сгенерирован тестовый файл tests/contract.spec.ts
для тестирования контракта:
import ...
describe('Fireworks', () => {
...
expect(deployResult.transactions).toHaveTransaction({
...
});
});
it('should deploy', async () => {
// the check is done inside beforeEach
// blockchain and fireworks are ready to use
});
Тесты запускаются с помощью следующей команды:
npx blueprint test
Дополнительные опции и vmLogs могут быть указаны с помощью blockchain.verbosity
:
blockchain.verbosity = {
...blockchain.verbosity,
blockchainLogs: true,
vmLogs: 'vm_logs_full',
debugLogs: true,
print: false,
}
Прямые модульные тесты
Fireworks демонстрирует различные операции с отправкой сообщений в блокчейне TON.
После развертывания с сообщением set_first
с достаточной суммой TON, он буд ет автоматически запущен с основными и пригодными для использования комбинациями режимов отправки.
Fireworks переразвернулся, в результате чего будет создано 3 сущности Fireworks, каждая из которых имеет свой ID(держите его в хранилище) и, как следствие, свой адрес смарт-контракта.
Для наглядности определите разные по ID экземпляры Fireworks (разные state_init
) со следующими именами:
- 1 - Fireworks setter - сущность, которая распределяет различные опкоды запуска. Может быть расширено до четырех различных опкодов.
- 2 - Fireworks launcher-1 - экземпляр Fireworks, который запускает первый fireworks, что означает, что сообщения будут отправляться на launcher.
- 3 - Fireworks launcher-2 - экземпляр Fireworks, который запускает второй fireworks, что означает, что сообщения будут отправляться из launcher.
Развернуть детали транзакций
index - ID транзакции в массиве launchResult
.
0
- Внешний запрос к казне (Launcher), который привел к исходящему сообщениюop::set_first
с 2.5 до fireworks1
- Транзакция в контракте Fireworks setter вызвана с помощьюop::set_first
и выполнена с двумя исходящими сообщениями для Fireworks Launcher-1 и Fireworks Launcher-22
- Транзакция в Fireworks launcher 1 вызвана с помощьюop::launch_first
, и выполнена с четырьмя исходящими сообщениями в Launcher.3
- Транзакция в Fireworks launcher 2 вызвана с помощьюop::launch_second
и выполнена с исходящим сообщением для Launcher.4
- Транзакция в Launcher с входящим сообщением от Fireworks launcher 1. Это сообщение отправлено сsend mode = 0
.5
- Транзакция в Launcher с входящим сообщением от Fireworks launcher 1. Это сообщение отправлено сsend mode = 1
.6
- Транзакция в Launcher с входящим сообщением от Fireworks launcher 1. Это сообщение отправлено сsend mode = 2
.7
- Транзакция в Launcher с входящим сообщением от Fireworks launcher 1. Это сообщение отправлено сsend mode = 128 + 32
.8
- Транзакция в Launcher с входящим сообщением от Fireworks launcher 2. Это сообщение отправлено сsend mode = 64
.
Каждый "firework" - исходящее сообщение с уникальным телом сообщения - появляется в транзакциях с ID:3 и ID:4.
Ниже приведен список тестов для каждой транзакции, ожидаемой как успешно выполненная. Транзакция[ID:0] Внешний запрос к к азне (Launcher), завершившийся исходящим сообщением op::set_first
с 2.5 к Fireworks. В случае, если Вы развернете Fireworks на блокчейне, fireworks - это Ваш кошелек.
Транзакция ID:1 Успешный тест
Этот тест проверяет, успешно ли установлен firework, отправляя транзакцию со значением 2.5 TON. Это простейший случай, основная цель здесь - утвердить результат свойства success транзакции в true.
Чтобы отфильтровать определенную транзакцию из массива launhcResult.transactions
, мы можем использовать наиболее убедительные поля.
С помощью
from
(адрес отправителя контракта), to
(адрес получателя контракта), op
(значение опкода) - мы получим только одну транзакцию для этой комбинации.
Транзакция [ID:1] в контракте Fireworks Setter, вызвана через op::set_first
и выполнена с двумя исходящими сообщениями к Fireworks launcher-1 и Fireworks launcher-2
it('first transaction[ID:1] should set fireworks successfully', async () => {
const launcher = await blockchain.treasury('launcher');
const launchResult = await fireworks.sendDeployLaunch(launcher.getSender(), toNano('2.5'));
expect(launchResult.transactions).toHaveTransaction({
from: launcher.address,
to: fireworks.address,
success: true,
op: Opcodes.set_first
})
});
Транзакция ID:2 Успешный тест
Этот тест проверяет, успешно ли выполнена транзакция[ID:2].
Транзакция в Fireworks launcher 1 вызвана через op::launch_first
, и выполняется четырьмя исходящими сообщениями в Launcher.
it('should exist a transaction[ID:2] which launch first fireworks successfully', async () => {
const launcher = await blockchain.treasury('launcher');
const launchResult = await fireworks.sendDeployLaunch(launcher.getSender(), toNano('2.5'));
expect(launchResult.transactions).toHaveTransaction({
from: fireworks.address,
to: launched_f1.address,
success: true,
op: Opcodes.launch_first,
outMessagesCount: 4,
destroyed: true,
endStatus: "non-existing",
})
printTransactionFees(launchResult.transactions);
});
В случаях, когда транзакция должна повлиять на состояние контракта, это можно указать с помощью полей destroyed
, endStatus
.
Полный список полей, связанных со статусом аккаунта:
destroyed
-true
- если существующий контракт был уничтожен в результате выполнения определенной транзакции. В противном случае -false
.deploy
- флаг песочницы, указывающий, был ли контракт развернут во время этой транзакции.true
, если контракт до этой транзакции не был инициализирован, а после этой транзакции стал инициализированным. В противном случае -false
.oldStatus
- статус аккаунта до выполнения транзакции. Значения:uninitialized
,frozen
,active
,non-existing
.endStatus
- статус аккаунта после выполнения транзакции. Значения:uninitialized
,frozen
,active
,non-existing
.
Транзакция ID:3 Успешный тест
Этот тест проверяет, успешно ли выполнена транзакция[ID:3].
Транзакция [ID:3] осуществляется в Fireworks launcher 1, вызывается через op::launch_first
и выполняется четырьмя исходящими сообщениями в Launcher.
it('should exist a transaction[ID:3] which launch second fireworks successfully', async () => {
const launcher = await blockchain.treasury('launcher');
const launchResult = await fireworks.sendDeployLaunch(launcher.getSender(), toNano('2.5'));
expect(launchResult.transactions).toHaveTransaction({
from: fireworks.address,
to: launched_f2.address,
success: true,
op: Opcodes.launch_second,
outMessagesCount: 1
})
printTransactionFees(launchResult.transactions);
});
Транзакция ID:4 Успешный тест
Этот тест проверяет, успешно ли выполнена транзакция[ID:4].
Транзакция [ID:4] осуществляется в Launcher(Deploy Wallet) с входящим сообщением от Fireworks launcher 1. Это сообщение отправлено с send mode = 0
в транзакции[ID:2].
it('should exist a transaction[ID:4] with a comment send mode = 0', async() => {
const launcher = await blockchain.treasury('launcher');
const launchResult = await fireworks.sendDeployLaunch(
launcher.getSender(),
toNano('2.5'),
);
expect(launchResult.transactions).toHaveTransaction({
from: launched_f1.address,
to: launcher.address,
success: true,
body: beginCell().storeUint(0,32).storeStringTail("send mode = 0").endCell() // 0x00000000 comment opcode and encoded comment
});
})
Транзакция ID:5 Успешный тест
Этот тест проверяет, успешно ли выполнена транзакция[ID:5].
Транзакция[ID:5] осуществляется в Launcher с входящим сообщением от Fireworks launcher 1. Это сообщение отправлено с send mode = 1
it('should exist a transaction[ID:5] with a comment send mode = 1', async() => {
const launcher = await blockchain.treasury('launcher');
const launchResult = await fireworks.sendDeployLaunch(
launcher.getSender(),
toNano('2.5'),
);
expect(launchResult.transactions).toHaveTransaction({
from: launched_f1.address,
to: launcher.address,
success: true,
body: beginCell().storeUint(0,32).storeStringTail("send mode = 1").endCell() // 0x00000000 comment opcode and encoded comment
});
})
Транзакция ID:6 Успешный тест
Этот тест проверяет, успешно ли выполнена транзакция[ID:6].
Транзакция[ID:6] осуществляется в Launcher с входящим сообщением от Fireworks launcher 1. Это сообщение отправлено с send mode = 2
it('should exist a transaction[ID:6] with a comment send mode = 2', async() => {
const launcher = await blockchain.treasury('launcher');
const launchResult = await fireworks.sendDeployLaunch(
launcher.getSender(),
toNano('2.5'),
);
expect(launchResult.transactions).toHaveTransaction({
from: launched_f1.address,
to: launcher.address,
success: true,
body: beginCell().storeUint(0,32).storeStringTail("send mode = 2").endCell() // 0x00000000 comment opcode and encoded comment
});
})
Транзакция ID:7 Успешный тест
Этот тест проверяет, успешно ли выполнена транзакция[ID:7].
Транзакция[ID:7] осуществляется в Launcher с входящим сообщением от Fireworks launcher 1. Это сообщение отправлено с send mode = 128 + 32
it('should exist a transaction[ID:7] with a comment send mode = 32 + 128', async() => {
const launcher = await blockchain.treasury('launcher');
const launchResult = await fireworks.sendDeployLaunch(
launcher.getSender(),
toNano('2.5'),
);
expect(launchResult.transactions).toHaveTransaction({
from: launched_f1.address,
to: launcher.address,
success: true,
body: beginCell().storeUint(0,32).storeStringTail("send mode = 32 + 128").endCell() // 0x00000000 comment opcode and encoded comment
});
})
Транзакция ID:8 Успешный тест
Этот тест проверяет, успешно ли выполнена транзакция[ID:8].
Транзакция[ID:8] осуществляется в Launcher с входящим сообщением от Fireworks launcher 2. Это сообщение отправлено с send mode = 64
it('should exist a transaction[ID:8] with a comment send mode = 64', async() => {
const launcher = await blockchain.treasury('launcher');
const launchResult = await fireworks.sendDeployLaunch(
launcher.getSender(),
toNano('2.5'),
);
expect(launchResult.transactions).toHaveTransaction({
from: launched_f2.address,
to: launcher.address,
success: true,
body: beginCell().storeUint(0,32).storeStringTail("send_mode = 64").endCell() // 0x00000000 comment opcode and encoded comment
});
})
Сборы за транзакции при выводе и чтении
Во время тестирования чтение информации о комиссионных может быть полезно для оптимизации контракта. Функция printTransactionFees печатает всю цепочку транзакций в удобном виде."
it('should be executed and print fees', async() => {
const launcher = await blockchain.treasury('launcher');
const launchResult = await fireworks.sendDeployLaunch(
launcher.getSender(),
toNano('2.5'),
);
console.log(printTransactionFees(launchResult.transactions));
});
Например, в случае launchResult
будет выведена следующая таблица:
(index) | op | valueIn | valueOut | totalFees | outActions |
---|---|---|---|---|---|
0 | 'N/A' | 'N/A' | '2.5 TON' | '0.010605 TON' | 1 |
1 | '0x5720cfeb' | '2.5 TON' | '2.185812 TON' | '0.015836 TON' | 2 |
2 | '0x6efe144b' | '1.092906 TON' | '1.081142 TON' | '0.009098 TON' | 4 |
3 | '0xa2e2c2dc' | '1.092906 TON' | '1.088638 TON' | '0.003602 TON' | 1 |
4 | '0x0' | '0.099 TON' | '0 TON' | '0.000309 TON' | 0 |
5 | '0x0' | '0.1 TON' | '0 TON' | '0.000309 TON' | 0 |
6 | '0x0' | '0.099 TON' | '0 TON' | '0.000309 TON' | 0 |
7 | '0x0' | '0.783142 TON' | '0 TON' | '0.000309 TON' | 0 |
8 | '0x0' | '1.088638 TON' | '0 TON' | '0.000309 TON' | 0 |
index - идентификатор транзакции в массиве launchResult
.
0
- Внешний запрос к казне (Launcher), который привел к сообщениюop::set_first
для Fireworks1
- Транзакция Fireworks, которая привела к 4 сообщениям для Launcher2
- Транзакция на Launched Fireworks - 1 от Launcher, сообщение отправлено с кодом операцииop::launch_first
.2
- Транзакция на Launched Fireworks - 2 от Launcher, сообщение отправлено с кодом операцииop::launch_second
.4
- транзакция на Launcher с входящим сообщением от Launched Fireworks - 1, сообщение отправлено сsend mode = 0
5
- Транзакция на Launcher с входящим сообщением от Launched Fireworks - 1, сообщение отправлено сsend mode = 1
6
- Транзакция на Launcher с входящим сообщением от Launched Fireworks - 1, сообщение отправлено сsend mode = 2
7
- Транзакция на Launcher с входящим сообщением от Launched Fireworks - 1, сообщение отправлено сsend mode = 128 + 32
8
- Транзакция на Launcher с входящим сообщением от Launched Fireworks - 2, сообщение отправлено сsend mode = 64
Тесты комиссий за транзакции
Этот тест проверяет, соответствуют ли ожиданиям комиссионные за запуск фейерверка (launching the fireworks). Можно определить свои проверки для различных частей комиссионных сборов.
it('should be executed with expected fees', async() => {
const launcher = await blockchain.treasury('launcher');
const launchResult = await fireworks.sendDeployLaunch(
launcher.getSender(),
toNano('2.5'),
);
//totalFee
console.log('total fees = ', launchResult.transactions[1].totalFees);
const tx1 = launchResult.transactions[1];
if (tx1.description.type !== 'generic') {
throw new Error('Generic transaction expected');
}
//computeFee
const computeFee = tx1.description.computePhase.type === 'vm' ? tx1.description.computePhase.gasFees : undefined;
console.log('computeFee = ', computeFee);
//actionFee
const actionFee = tx1.description.actionPhase?.totalActionFees;
console.log('actionFee = ', actionFee);
if ((computeFee == null || undefined) ||
(actionFee == null || undefined)) {
throw new Error('undefined fees');
}
//The check, if Compute Phase and Action Phase fees exceed 1 TON
expect(computeFee + actionFee).toBeLessThan(toNano('1'));
});
Тесты пограничных случаев
В этом разделе будут приведены тестовые случаи для кодов выхода TVM, которые могут возникнуть во время обработки транзакции. Эти коды выхода находятся в самом коде блокчейна. При этом необходимо различать код выхода во время Compute Phase и код выхода во время Action Phase.
Во время Compute Phase выполняется логика контракта (его код). В процессе обработки могут быть созданы различные действия. Эти действия будут обработаны в следующей фазе - Action Phase. Если Compute Phase завершилась неудачей, то Action Phase не начинается. Однако если Compute Phase прошла успешно, это не гарантирует, что Action Phase также завершится успешно.
Compute Phase | код выхода = 0
Этот код выхода означает, что фаза вычислений транзакции была успешно завершена.
Compute Phase | код выхода = 1
Альтернативный код выхода, обозначающий успех фазы вычислений, - 1
. Чтобы получить этот код выхода, Вам необходимо использовать RETALT.
Следует отметить, что этот опкод должен вызываться в главной функции (например, recv_internal). Если Вы вызовете его в другой функции, то выход из этой функции будет 1
, но общий код выхода будет 0
.
Compute Phase | код выхода = 2
TVM - это стековая машина. При взаимодействии с различными значениями они появляются в стеке. Если вдруг в стеке нет элементов, но какой-то опкод их требует, то будет выброшена эта ошибка.
Это может произойти при работе с опкодами напрямую, поскольку stdlib.fc (библиотека для FunC) предполагает, что такой проблемы не возникнет.
Compute Phase | код выхода = 3
Любой код перед выполнением становится continuation
. Это специальный тип данных, который содержит фрагмент с кодом, стек, регистры и другие данные, необходимые для выполнения кода. При необходимости его можно запустить позже, передав необходимые параметры для начального состояния стека.
Сначала мы построим continuation. В данном случае это просто пустой continuation, который ничего не делает. Далее, используя опкод 0 SETNUMARGS
, мы указываем, что в начале выполнения в стеке не должно быть никаких значений. Затем, используя опкод 1 -1 SETCONTARGS
, мы вызываем continuation, передавая 1 значение. Поскольку значений не должно было быть, мы получаем ошибку StackOverflow.
Compute Phase | код выхода = 4
В TVM значение integer
может находиться в диапазоне -2256 < x < 2256. Если значение при вычислении вышло за пределы этого диапазона, то будет выброшен код выхода 4.
Compute Phase | код выхода = 5
Если значение integer
вышло за пределы ожидаемого диапазона, то будет выброшен код выхода 5. Например, если в функции .store_uint()
было использовано отрицательн ое значение.
Compute Phase | код выхода = 6
На более низком уровне вместо привычных имен функций используются опкоды, которые можно увидеть в этой таблице в HEX-формате. В этом примере мы используем @addop
, который добавляет несуществующий опкод.
Эмулятор, пытаясь обработать этот опкод, не распознает его и выбрасывает код 6.
Compute Phase | код выхода = 7
Это довольно распространенная ошибка, которая возникает при получении неправильного типа данных. В примере речь идет о случае, когда tuple
содержал 3 элемента, но при распаковке была попытка получить 4.
Существует множество других случаев, когда возникает эта ошибка. Вот некоторые из них:
- не null
- не integer
- не cell
- не cell builder
- не cell slice
- не string
- не bytes chunk
- не continuation
- не box
- не tuple
- не atom
Compute Phase | код выхода = 8
Все данные в TON хранятся в ячейках (cells). Ячейка может хранить 1023 бита данных и 4 ссылки на другие ячейки. Если Вы попытаетесь записать более 1023 бит или более 4 ссылок, будет выброшен код выхода 8.
Compute Phase | код выхода = 9
Если Вы попытаетесь прочитать из фрагмента (cell slice) больше данных (при чтении данных из ячейки они должны быть преобразованы к типу данных фрагмента), чем он содержит, то будет выброшен код завершения 9. Например, если в cell slice было 10 битов, а прочитано 11, или если не было ссылок на другие ссылки, но была попытка загрузить ссылку.
Compute Phase | код выхода = 10
Эта ошибка возникает при работе со словарями (dictionaries). В качестве примера можно привести случай, когда значение, принадлежащее ключу хранится в другой ячейке в виде ссылки. В этом случае для получения такого значения Вам необходимо использовать функцию .udict_get_ref()
.
Однако ссылка на другую ячейку должна быть только 1, а не 2, как в нашем примере:
root_cell
├── key
│ ├── value
│ └── value - second reference for one key
└── key
└── value
Поэтому при попытке прочитать значение мы получаем код выхода 10.
Дополнительно: Вы также можете хранить значение рядом с ключом в словаре:
root_cell
├── key-value
└── key-value