合约测试

测试在软件开发中起着至关重要的作用,尤其是对于智能合约而言。在本节中,我们将通过Starknet上的scarb ,引导你了解智能合约测试的基础知识。

让我们以一个简单的智能合约作为例子开始:

use starknet::ContractAddress;

#[starknet::interface]
trait ISimpleContract<TContractState> {
    fn get_value(self: @TContractState) -> u32;
    fn get_owner(self: @TContractState) -> ContractAddress;
    fn set_value(ref self: TContractState, value: u32);
}

#[starknet::contract]
mod SimpleContract {
    use starknet::{get_caller_address, ContractAddress};

    #[storage]
    struct Storage {
        value: u32,
        owner: ContractAddress
    }

    #[constructor]
    fn constructor(ref self: ContractState, initial_value: u32) {
        self.value.write(initial_value);
        self.owner.write(get_caller_address());
    }

    #[abi(embed_v0)]
    impl SimpleContract of super::ISimpleContract<ContractState> {
        fn get_value(self: @ContractState) -> u32 {
            self.value.read()
        }

        fn get_owner(self: @ContractState) -> ContractAddress {
            self.owner.read()
        }

        fn set_value(ref self: ContractState, value: u32) {
            assert(self.owner.read() == get_caller_address(), ‘Not owner’);
            self.value.write(value);
        }
    }
}

现在,让我们看一下这个合约的测试:

#[cfg(test)]
mod tests {
    // Import the interface and dispatcher to be able to interact with the contract.
    use testing_how_to::contract::{
        ISimpleContract, SimpleContract, ISimpleContractDispatcher, ISimpleContractDispatcherTrait
    };

    // Import the deploy syscall to be able to deploy the contract.
    use starknet::class_hash::Felt252TryIntoClassHash;
    use starknet::{
        deploy_syscall, ContractAddress, get_caller_address, get_contract_address,
        contract_address_const
    };

    // Use starknet test utils to fake the transaction context.
    use starknet::testing::{set_caller_address, set_contract_address};

    // Deploy the contract and return its dispatcher.
    fn deploy(initial_value: u32) -> ISimpleContractDispatcher {
        // Set up constructor arguments.
        let mut calldata = ArrayTrait::new();
        initial_value.serialize(ref calldata);

        // Declare and deploy
        let (contract_address, _) = deploy_syscall(
            SimpleContract::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata.span(), false
        )
            .unwrap();

        // Return the dispatcher.
        // The dispatcher allows to interact with the contract based on its interface.
        ISimpleContractDispatcher { contract_address }
    }

    #[test]
    #[available_gas(2000000000)]
    fn test_deploy() {
        let initial_value: u32 = 10;
        let contract = deploy(initial_value);

        assert(contract.get_value() == initial_value, 'wrong initial value');
        assert(contract.get_owner() == get_contract_address(), 'wrong owner');
    }

    #[test]
    #[available_gas(2000000000)]
    fn test_set_as_owner() {
        // Fake the caller address to address 1
        let owner = contract_address_const::<1>();
        set_contract_address(owner);

        let contract = deploy(10);
        assert(contract.get_owner() == owner, 'wrong owner');

        // Fake the contract address to address 1
        set_contract_address(owner);
        let new_value: u32 = 20;
        contract.set_value(new_value);

        assert(contract.get_value() == new_value, 'wrong value');
    }

    #[test]
    #[should_panic]
    #[available_gas(2000000000)]
    fn test_set_not_owner() {
        let owner = contract_address_const::<1>();
        set_contract_address(owner);

        let contract = deploy(10);

        let not_owner = contract_address_const::<2>();
        set_contract_address(not_owner);

        let new_value: u32 = 20;
        contract.set_value(new_value);
    }
}

Play with this contract in Remix.

为了定义我们的测试,我们使用 scarb,它允许我们创建一个被 #[cfg(test)] 保护的独立模块。这样可以确保测试模块只在使用 scarb test 运行测试时被编译。

每个测试都被定义为带有 #[test] 属性的函数。您还可以使用 #[should_panic] 属性检查测试是否会引发 panic。

由于我们处于智能合约的上下文中,设置 gas 限制非常重要。你可以通过使用 #[available_gas(X)] 属性来指定测试的 gas 限制。这也是确保合约功能保持在某个特定 gas 限制下的好方法!

注意:这里的 “gas” 一词指的是 Sierra gas,而不是 L1 的 gas

现在,让我们进入测试过程:

  • 使用 deploy 函数的逻辑来声明和部署您的合约。
  • 使用 assert 来验证合约在给定的上下文中的行为是否符合预期。

为了使测试更加方便,corelib 的 testing 模块提供了一些有用的函数:

  • set_caller_address(address: ContractAddress)
  • set_contract_address(address: ContractAddress)
  • set_block_number(block_number: u64)
  • set_block_timestamp(block_timestamp: u64)
  • set_account_contract_address(address: ContractAddress)
  • set_max_fee(fee: u128)

你可能还需要 corelib 中的 info 模块,它允许你访问有关当前交易上下文的信息:

  • get_caller_address() -> ContractAddress
  • get_contract_address() -> ContractAddress
  • get_block_info() -> Box<BlockInfo>
  • get_tx_info() -> Box<TxInfo>
  • get_block_timestamp() -> u64
  • get_block_number() -> u64

你可以在 Starknet Corelib 仓库 中找到完整的函数列表。 你还可以在 Cairo book 第8章 中找到有关在 cairo 中进行测试的详细说明。

Starknet Foundry

<!— TODO update this when Starknet Foundry is more mature. —>

Starknet Foundry是在Starknet上开发智能合约的强大工具包。它提供了对使用Forge 工具在 scarb 上测试Starknet智能合约的支持。

使用 snforge 进行测试与我们刚刚描述的过程类似,但更简化。此外,还有其他功能正在开发中,包括作弊码或并行测试执行。我们强烈推荐探索Starknet Foundry并将其纳入您你的项目中。

有关使用Starknet Foundry测试合约的更详细信息,请参阅Starknet Foundry Book - 合约测试

Last change: 2023-12-06, commit: 1af1816