编写测试示例
此页面展示了如何为在Blueprint SDK(Sandbox)中创建的FunC合约编写测试。
测试套件为演示合约fireworks构建。Fireworks是一个通过set_first
消息初始化运行的智能合约。
通过npm create ton@latest
创建一个新的FunC项目后,测试文件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
可以通过blockchain.verbosity
指定附加选项和vmLogs:
blockchain.verbosity = {
...blockchain.verbosity,
blockchainLogs: true,
vmLogs: 'vm_logs_full',
debugLogs: true,
print: false,
}
直接 cell 测试
Fireworks演示了在TON区块链中发送消息的不同操作。
一旦你有足够TON金额并通过set_first
消息部署它,它将使用主要和可用的发送模式组合自动执行。
Fireworks重新部署自己,结果将创建3个Fireworks实体,每个实体都有自己的ID(被保存在存储中),因此有不同的智能合约地址。
为了清晰起见,我们定义不同ID的Fireworks实例(不同的state_init
)并以下列名称命名:
- 1 - Fireworks setter - 传播不同启动操作码的实体。可以扩展到四种不同的操作码。
- 2 - Fireworks launcher-1 - 启动第一个firework的Fireworks实例,意味着消息将被发送给launcher。
- 3 - Fireworks launcher-2 - 启动第二个firework的Fireworks实例,意味着消息将被发送给launcher。
展开交易细节
index - 是launchResult
数组中交易的ID。
0
- 对资金库(the Launcher)的外部请求,导致向fireworks发送2.5 TON的出站消息op::set_first
1
- 在Fireworks setter合约中使用op::set_first
调用的交易,并执行了两个出站消息到Fireworks Launcher-1和Fireworks Launcher-22
- 在Fireworks launcher 1中使用op::launch_first
调用的交易,并执行了四个出站消息到the Launcher。3
- 在Fireworks launcher 2中使用op::launch_second
调用的交易,并执行了一个出站消息到the Launcher。4
- 在the Launcher中来自Fireworks launcher 1的入站消息的交易。此消息以send mode = 0
发送。5
- 在the Launcher中来自Fireworks launcher 1的入站消息的交易。此消息以send mode = 1
发送。6
- 在the Launcher中来自Fireworks launcher 1的入站消息的交易。此消息以send mode = 2
发送。7
- 在the Launcher中来自Fireworks launcher 1的入站消息的交易。此消息以send mode = 128 + 32
发送。8
- 在the Launcher中来自Fireworks launcher 2的入站消息的交易。此消息以send mode = 64
发送。
每个“firework” - 是交易ID:3和ID:4中出现的带有独特消息体的出站消息。
以下是每个预期成功执行的交易的测试列表。交易[ID:0]是对资金库(the Launcher)的外部请求,导致向fireworks发送2.5 TON的出站消息op::set_first
。如果您将Fireworks部署到区块链,launcher会是您的钱包。
交易ID:1 成功测试
此测试检查是否通过发送2.5 TON的交易成功设置了fireworks。 这是最简单的情况,主要目的是确认交易成功属性为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]是否成功执行。
交易[ID:2]在Fireworks launcher 1中进行,用op::launch_first
调用,并执行了四个出站消息到the 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
调用,并执行了四个出站消息到the 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]是否成功执行。
收到来自Fireworks launcher 1的入站消息,交易[ID:4]在the Launcher(部署钱包)中进行。此消息以send mode = 0
发送。
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]是否成功执行。
收到来自Fireworks launcher 1的入站消息,交易[ID:5]在the Launcher中进行。此消息以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]是否成功执行。
收到来自Fireworks launcher 1的入站消息,交易[ID:6]在the Launcher中进行。此消息以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]是否成功执行。
收到来自Fireworks launcher 1的入站消息,交易[ID:7]在the Launcher中进行。此消息以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]是否成功执行。
收到来自Fireworks launcher 2的入站消息,交易[ID:8]在the Launcher中进行。此消息以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
数组中交易的ID。
0
- 对资金库(the Launcher)的外部请求,导致发送消息op::set_first
到Fireworks1
- 导致发送4条消息到the Launcher的Fireworks交易2
- 在Launched Fireworks - 1中从the Launcher收到消息,消息使用op::launch_first
操作码发送。2
- 在Launched Fireworks - 2中从the Launcher收到消息,消息使用op::launch_second
操作码发送。4
- 在the Launcher中收到来自Launched Fireworks - 1的消息的交易,消息以send mode = 0
发送5
- 在the Launcher中收到来自Launched Fireworks - 1的消息的交易,消息以send mode = 1
发送6
- 在the Launcher中收到来自Launched Fireworks - 1的消息的交易,消息以send mode = 2
发送7
- 在the Launcher中收到来自Launched Fireworks - 1的消息的交易,消息以send mode = 128 + 32
发送8
- 在the Launcher中收到来自Launched Fireworks - 2的消息的交易,消息以send mode = 64
发送
交易费用测试
此测试验证启动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 exit codes(退出代码)的测试用例。这些exit codes在区块链代码本身中。同时,必须区分在Compute Phase( Compute Phase )和Action Phase(行动阶段)期间的exit code。
Compute Phase期间执行合约逻辑(其代码)。在处理期间,可以创建不同的action(动作)。这些action将在下一阶段 - Action Phase处理。如果Compute Phase不成功,则Action Phase不开始。然而,如果Compute Phase成功,这并不保证Action Phase也会成功结束。
Compute Phase | exit code = 0
此exit code表示交易的Compute Phase已成功完成。
Compute Phase | exit code = 1
标记Compute Phase成功的另一种exit code是1
。要获得此exit code,您需要使用RETALT。
值得注意的是,这个操作码应该在主函数中调用(例如,recv_internal)。如果在另一个函数中调用,则该函数的exit将为1
,但总体exit code将为0