相比于编写合约,编写合约测试也是相当费时费力的,一个合格的FunC工程师必须要投入solid time for testing。先来回顾一下之前写的测试。
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()); }); });
现在如果再跑一遍 yarn test 是会报错的,因为合约里的代码已经改动很大了。所以第一步要做的就是对测试文件进行调整:
- 提供正确的initial state data,让c4 储存空间预存一些数据。可以将计数器的值设置为0,将最近的信息发送者地址设置为0地址(zeroAddress)
- 在对合约发送信息时要包含opcode操作码
先处理initial data。之前通过调用 createFromConfig 函数的方法初始化合约,并且将合约的state 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); }
然后在main.spec.ts中调用了createFromConfig。
const myContract = blockchain.openContract( await MainContract.createFromConfig({}, codeCell) );
目前只传递了一个空的config,现在尝试将需要的值传递进去来正确的初始化合约。
定义一下config应该看起来是啥样。在导入 wrappers/MainContract.ts 后定义一个新type,MainContractConfig。
// ... library imports export type MainContractConfig = { number: number; address: Address; };
另外一个很有用的函数就是mainContractConfigToCell,它会接受MainContractConfig,并且将其中的参数打包进cell中再return回来。
// ... library imports // ... MainContractConfig type export function mainContractConfigToCell(config: MainContractConfig): Cell { return beginCell().storeUint(config.number, 32).storeAddress(config.address).endCell(); }
现在更新下createFromConfig,它现在要用mainContractConfigToCell函数来生成初始的initial data cell。
// ... library imports // ... MainContractConfig type // ... mainContractConfigToCell function export class MainContract implements Contract { constructor( readonly address: Address, readonly init?: { code: Cell; data: Cell } ) {} static createFromConfig( config: MainContractConfig, code: Cell, workchain = 0 ) { const data = mainContractConfigToCell(config); const init = { code, data }; const address = contractAddress(workchain, init); return new MainContract(address, init); } // ... rest of the methods }
现在的createFromConfig函数就完成了,再将tests/main.spec.ts文件中对该函数的调用更新一下。
// ... library imports 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 initAddress = await blockchain.treasury("initAddress"); const myContract = blockchain.openContract( await MainContract.createFromConfig( { number: 0, address: initAddress.address, }, codeCell ) ); // ... the rest of old tests logic }
注意是我通过种子“initAddress”创建了一个initAddress钱包来将它的地址传入mainContract中的createFromConfig函数中。
目前已经成功初始化了合约。
接下来还需要在信息主体中插入特定的数据 - -所以还需要更新下wrapper/MainContract..ts中的senInternalMessage方法。
将函数名改成sendIncrement,它将接受更多的值,increment_by。代表当前合约的存储值storage value会按多少递增。
还需要注意这段message boy中还包含了opcode操作码,so,记得初始化一段32bit integer的存储空间给opcode,再初始化32bit integer留给incrementing value递增值。
async sendIncrement( provider: ContractProvider, sender: Sender, value: bigint, increment_by: number ) { const msg_body = beginCell() .storeUint(1, 32) // OP code .storeUint(increment_by, 32) // increment_by value .endCell(); await provider.internal(sender, { value, sendMode: SendMode.PAY_GAS_SEPARATELY, body: msg_body, }); }
另外还有一个函数需要更新的就是getData,因为它现在需要返回两个值了。
async getData(provider: ContractProvider) { const { stack } = await provider.get("get_contract_storage_data", []); return { number: stack.readNumber(), recent_sender: stack.readAddress(), }; }
此处我们用了readNumber()的方法来读取当前的整数值。
截止到目前为止,已经可以返回tests/main.spec.ts中来最后完成测试了。更新后的测试代码如下:
// ... library imports describe("main.fc contract tests", () => { it("should successfully increase counter in contract and get the proper most recent sender address", async () => { const blockchain = await Blockchain.create(); const codeCell = Cell.fromBoc(Buffer.from(hex, "hex"))[0]; const initAddress = await blockchain.treasury("initAddress"); const myContract = blockchain.openContract( await MainContract.createFromConfig( { number: 0, address: initAddress.address, }, codeCell ) ); const senderWallet = await blockchain.treasury("sender"); const sentMessageResult = await myContract.sendIncrement( senderWallet.getSender(), toNano("0.05"), 1 ); 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()); expect(data.number).toEqual(1); }); });
这段代码更新了:
- 传递了一个值:1,到 sendIncrement函数中,所以resulting value递增会逐次+1。
- 为recent_sender和number value设置了expectations 。
现在再来run一次yarn test的话,应该能看见如下的结果:
PASS tests/main.spec.ts main.fc contract tests ✓ should successfully increase counter in contract and get the proper most recent sender address (452 ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 4.339 s, estimated 5 s