Create Smart Contracts on TON - Lesson 6 Tests for FunC smart contract with op and query_id

Introduction

In this lesson, we will write tests for the smart contract created in the fifth lesson on the TON blockchain and run them using Blueprint.

Requirements

To complete this lesson, you only need to install Node.js. It is recommended to install one of the latest versions, such as 18.

You should also complete the fifth lesson.

Task of the fifth lesson

For convenience, let me remind you here what we did in the fifth lesson. The task of the smart contract is to remember the address set by the manager and provide it to anyone who requests it, with the following functionality:

  • when the contract receives a message from the Manager with op equal to 1, followed by some query_id, followed by MsgAddress, it should save the received address in storage.
  • when the contract receives an internal message from any address with op equal to 2, followed by query_id, it should respond to the sender with a message body containing:
    • op equal to 3
    • the same query_id
    • the Manager’s address
    • the address that was remembered since the last manager request (empty address addr_none if there hasn’t been a manager request yet)
    • the TON value attached to the message minus the processing fee.
  • when the smart contract receives any other message, it should throw an exception.

Tests for the smart contract with op and query_id

For our proxy smart contract, we will write the following tests:

  • Saving addresses with op = 1
  • Only the manager should be able to save the address
  • Handling op = 2
  • The contract should throw an exception for any other opcode

Before writing the tests…

Open the file tests/AddressSaver.spec.ts (the name may be different if you named the project differently), where our tests will be. Remember from the fourth lesson that you need to update the values in the config when deploying to the ones we set there. Also, for convenience, you can move the declaration of the deployer object outside the beforeEach function to have access to it from all tests.

Testing op = 1

Generate a random address using the randomAddress() function, and then call the sendChangeAddress method on behalf of the deployer wallet.

In this case, there should be a transaction from deployer to AddressSaver with the success = true flag (which means that the execution of all transaction phases was successful).

it('should change saved address by manager', async () => {
    const address = randomAddress();
    const result = await addressSaver.sendChangeAddress(
        deployer.getSender(),
        toNano('0.01'),
        12345n,
        address
    );

    expect(result.transactions).toHaveTransaction({
        from: deployer.address,
        to: addressSaver.address,
        success: true,
    });
});

Testing exception when calling op = 1 by someone other than the manager

In this test, we do the same as in the previous one, but call sendChangeAddress on behalf of another wallet user.

In this case, the success flag should be false.

it('should not change saved address by anyone else', async () => {
    let user = await blockchain.treasury('user');
    const address = randomAddress();
    const result = await addressSaver.sendChangeAddress(
        user.getSender(),
        toNano('0.01'),
        12345n,
        address
    );

    expect(result.transactions).toHaveTransaction({
        from: user.address,
        to: addressSaver.address,
        success: false,
    });
});

Testing op = 2

Call sendChangeAddress as in the first test to successfully change the saved address. Then, using the new wallet user, call sendRequestAddress.

This call should trigger a transaction from AddressSaver to user with a message body that contains op = 3, query_id = 12345, deployer.address, address.

it('should return required data on `requestAddress` call', async () => {
    const address = randomAddress();
    await addressSaver.sendChangeAddress(
        deployer.getSender(),
        toNano('0.01'),
        12345n,
        address
    );

    let user = await blockchain.treasury('user');
    const result = await addressSaver.sendRequestAddress(
        user.getSender(),
        toNano('0.01'),
        12345n
    );
    expect(result.transactions).toHaveTransaction({
        from: addressSaver.address,
        to: user.address,
        body: beginCell()
            .storeUint(3, 32)
            .storeUint(12345n, 64)
            .storeAddress(deployer.address)
            .storeAddress(address)
            .endCell(),
    });
});

Testing exception for any other opcodes

For this test, we will use the send method of the Treasury contract. We will send a message with op = 5, for example.

Such a transaction should result in exitCode = 3, which we check in the test.

it('should throw on any other opcode', async () => {
    const result = await deployer.send({
        to: addressSaver.address,
        value: toNano('0.01'),
        body: beginCell().storeUint(5, 32).storeUint(12345n, 64).endCell(),
    });
    expect(result.transactions).toHaveTransaction({
        from: deployer.address,
        to: addressSaver.address,
        exitCode: 3,
    });
});

Run the tests with the command npx blueprint test and you should see the following:

 PASS  tests/AddressSaver.spec.ts
  AddressSaver
    ✓ should deploy (145 ms)
    ✓ should change saved address by manager (67 ms)
    ✓ should not change saved address by anyone else (67 ms)
    ✓ should return required data on `requestAddress` call (70 ms)
    ✓ should throw on any other opcode (89 ms)

If any of the tests did not pass, review the code and the text of this lesson again. Also, compare your smart contract code with the code from the previous lesson.

Conclusion

I would like to say a special thank you to those who donate to support the project, it is very motivating and helps to release lessons faster. If you want to help the project (release lessons faster, translate all this into English, etc.), there are donation addresses at the bottom of the main page.