如何测试合约
在将合约部署到链上之前,充分测试所有执行路径是必要的。本节介绍用 UTXO_Compiler 提供的工具进行本地测试的方法,以及组织测试用例的实践建议。
测试策略概览
UTXO 合约的测试重点与以太坊合约有所不同。由于合约逻辑全在链上执行,本地测试的核心目标是:
- 验证正常路径:合法输入应使合约返回真(花费被允许)
- 验证拒绝路径:不合法输入应被合约拒绝(返回假或直接终止)
- 验证边界条件:恰好在条件边界上的值
- 验证所有公有函数:每条花费路径都独立测试
- 验证字节码输出:编译结果符合预期的操作码序列
方法一:编译器冒烟测试
最基础的测试是直接让编译器编译合约文件,确认没有语法错误和所有权错误:
./utxo_compiler my_contract.ct如果输出 JSON 字节码而没有报错,说明合约至少在语法和语义层面是正确的。
如果你维护的是一个包含多份合约的项目,建议用批处理脚本做快速冒烟检查:
# 逐一编译 contracts/ 目录下的合约
COMPILER="${COMPILER:-utxo_compiler}"
for f in contracts/**/*.ct; do
echo "Testing: $f"
"$COMPILER" "$f" > /dev/null && echo " OK" || echo " FAILED"
done方法二:调试器手动测试
内置调试器 是目前最直接的执行验证手段。启动调试器,输入一组参数,让合约运行到结束,观察最终栈上的值是否为真:
./utxo_compiler my_contract.ct --debug测试矩阵
对于每个公有函数,建议按以下矩阵准备测试输入:
| 场景 | 预期结果 |
|---|---|
| 正确的签名 + 匹配的公钥 | 栈顶 = 1,通过 |
| 错误的签名 | 执行终止或栈顶 = 0 |
| 公钥哈希不匹配 | EqualVerify 失败,终止 |
| 空数据 / 零值 | 视合约逻辑而定 |
| 边界值(如时间刚好等于截止时间) | 验证 >= / > 的语义 |
方法三:编写测试合约文件集
对于需要持续集成的项目,建议将测试用例组织为独立的 .ct 文件,放在 tests/ 目录下,并配合 shell 脚本批量运行:
批量测试脚本
#!/bin/bash
# run_tests.sh
COMPILER="${COMPILER:-utxo_compiler}"
PASS=0
FAIL=0
for f in tests/**/*.ct; do
output=$($COMPILER "$f" 2>&1)
if [ $? -eq 0 ]; then
echo "PASS: $f"
((PASS++))
else
echo "FAIL: $f"
echo " Error: $output"
((FAIL++))
fi
done
echo ""
echo "Results: $PASS passed, $FAIL failed"
[ $FAIL -eq 0 ]方法四:调试信息辅助测试
使用 -d 标志编译,生成调试信息文件,然后配合调试器检查每一行的执行情况:
utxo_compiler my_contract.ct -d调试信息包含源码行号到字节码偏移的映射,可以用来:
- 确认某段代码是否被执行
- 检查循环的展开是否正确
- 验证条件分支的跳转逻辑
常见测试陷阱
陷阱一:所有权错误只在运行时触发
所有权检查是编译期的——编译器会在 utxo_compiler 阶段报错。测试时只需确认编译通过即可,不需要担心"运行时所有权错误"。
陷阱二:EqualVerify 失败不返回假,而是终止
EqualVerify、CheckSigVerify 等 *Verify 函数在失败时直接终止执行(等价于脚本执行失败)。测试拒绝场景时,这类函数触发的失败不会在栈上留下 0,而是让整个执行中止。调试器中会显示执行被终止。
陷阱三:合约成员变量是字节码的一部分
测试同一个合约逻辑的不同参数时,不需要重新编译,因为合约成员变量直接嵌在字节码里,需要用户手动进行替换。
陷阱四:循环次数是编译期常量
Range() 的参数必须是编译期可确定的值(字面量或常量表达式)。不能用变量控制循环次数。
下一步
- 如何调试合约 — 用交互式调试器深入检查执行过程