示例测试

以下是一些示例,可以让您更好地理解计划测试。

注意: 本节中的示例是为了协助您开发。我们不建议在没有自行验证的情况下依赖它们。

1. Simple example

在这个例子中,我们测试设置和获取变量。

要测试的合约/程序:Simple_storage.sol`

pragma solidity >=0.4.22 <0.7.0;

contract SimpleStorage {
    uint public storedData;

    constructor() public {
        storedData = 100;
    }

    function set(uint x) public {
        storedData = x;
    }

    function get() public view returns (uint retVal) {
        return storedData;
    }
}

测试合约/程序:simple_storage_test.sol`

pragma solidity >=0.4.22 <0.7.0;
import "remix_tests.sol";
import "./Simple_storage.sol";

contract MyTest {
    SimpleStorage foo;

    // beforeEach works before running each test
    function beforeEach() public {
        foo = new SimpleStorage();
    }

    /// Test if initial value is set correctly
    function initialValueShouldBe100() public returns (bool) {
        return Assert.equal(foo.get(), 100, "initial value is not correct");
    }

    /// Test if value is set as expected
    function valueIsSet200() public returns (bool) {
        foo.set(200);
        return Assert.equal(foo.get(), 200, "value is not 200");
    }
}

2.测试一个包含msg.sender的方法`

在Solidity中,msg.sender在智能合约方法交互的访问管理中扮演着重要的角色。不同的msg.sender可以帮助测试涉及多个不同角色的帐户的合约。以下是一个测试示例:

要测试的合约/程序:Sender.sol`

pragma solidity >=0.4.22 <0.7.0;
contract Sender {
    address private owner;
    
    constructor() public {
        owner = msg.sender;
    }
    
    function updateOwner(address newOwner) public {
        require(msg.sender == owner, "only current owner can update owner");
        owner = newOwner;
    }
    
    function getOwner() public view returns (address) {
        return owner;
    }
}

测试合约/程序:Sender_test.sol`

pragma solidity >=0.4.22 <0.7.0;
import "remix_tests.sol"; // this import is automatically injected by Remix
import "remix_accounts.sol";
import "./Sender.sol";

// Inherit 'Sender' contract
contract SenderTest is Sender {
    /// Define variables referring to different accounts
    address acc0;
    address acc1;
    address acc2;
    
    /// Initiate accounts variable
    function beforeAll() public {
        acc0 = TestsAccounts.getAccount(0); 
        acc1 = TestsAccounts.getAccount(1);
        acc2 = TestsAccounts.getAccount(2);
    }
    
    /// Test if initial owner is set correctly
    function testInitialOwner() public {
        // account at zero index (account-0) is default account, so current owner should be acc0
        Assert.equal(getOwner(), acc0, 'owner should be acc0');
    }
    
    /// Update owner first time
    /// This method will be called by default account(account-0) as there is no custom sender defined
    function updateOwnerOnce() public {
        // check method caller is as expected
        Assert.ok(msg.sender == acc0, 'caller should be default account i.e. acc0');
        // update owner address to acc1
        updateOwner(acc1);
        // check if owner is set to expected account
        Assert.equal(getOwner(), acc1, 'owner should be updated to acc1');
    }
    
    /// Update owner again by defining custom sender
    /// #sender: account-1 (sender is account at index '1')
    function updateOwnerOnceAgain() public {
        // check if caller is custom and is as expected
        Assert.ok(msg.sender == acc1, 'caller should be custom account i.e. acc1');
        // update owner address to acc2. This will be successful because acc1 is current owner & caller both
        updateOwner(acc2);
        // check if owner is set to expected account i.e. account2
        Assert.equal(getOwner(), acc2, 'owner should be updated to acc2');
    }
}

3. Testing method execution

使用 Solidity,人们可以通过从合约中检索这些变量来直接验证存储中的方法所做的更改。 但是测试一个成功的方法执行需要一些策略。 好吧,这并不完全正确,当测试成功时-通常很明显为什么它通过了。 但是,当测试失败时,必须了解它失败的原因。

为了在这种情况下提供帮助,Solidity在版本 0.6.0 中引入了try-catch语句。之前,我们必须使用低级调用来跟踪发生了什么。

这是一个使用try-catch代码块和低级调用的示例测试文件:

要测试的合约/程序:AttendanceRegister.sol

pragma solidity >=0.4.22 <0.7.0;
contract AttendanceRegister {
    struct Student{
            string name;
            uint class;
        }

    event Added(string name, uint class, uint time);

    mapping(uint => Student) public register; // roll number => student details

    function add(uint rollNumber, string memory name, uint class) public returns (uint256){
        require(class > 0 && class <= 12, "Invalid class");
        require(register[rollNumber].class == 0, "Roll number not available");
        Student memory s = Student(name, class);
        register[rollNumber] = s;
        emit Added(name, class, now);
        return rollNumber;
    }
    
    function getStudentName(uint rollNumber) public view returns (string memory) {
        return register[rollNumber].name;
    }
}

测试合约/程序:AttendanceRegister_test.sol

pragma solidity >=0.4.22 <0.7.0;
import "remix_tests.sol"; // this import is automatically injected by Remix.
import "./AttendanceRegister.sol";

contract AttendanceRegisterTest {
   
    AttendanceRegister ar;
    
    /// 'beforeAll' runs before all other tests
    function beforeAll () public {
        // Create an instance of contract to be tested
        ar = new AttendanceRegister();
    }
    
    /// For solidity version greater or equal to 0.6.0, 
    /// See: https://solidity.readthedocs.io/en/v0.6.0/control-structures.html#try-catch
    /// Test 'add' using try-catch
    function testAddSuccessUsingTryCatch() public {
        // This will pass
        try ar.add(101, 'secondStudent', 11) returns (uint256 r) {
            Assert.equal(r, 101, 'wrong rollNumber');
        } catch Error(string memory /*reason*/) {
            // This is executed in case
            // revert was called inside getData
            // and a reason string was provided.
            Assert.ok(false, 'failed with reason');
        } catch (bytes memory /*lowLevelData*/) {
            // This is executed in case revert() was used
            // or there was a failing assertion, division
            // by zero, etc. inside getData.
            Assert.ok(false, 'failed unexpected');
        }
    }
    
    /// Test failure case of 'add' using try-catch
    function testAddFailureUsingTryCatch1() public {
        // This will revert on 'require(class > 0 && class <= 12, "Invalid class");' for class '13'
        try ar.add(101, 'secondStudent', 13) returns (uint256 r) {
            Assert.ok(false, 'method execution should fail');
        } catch Error(string memory reason) {
            // Compare failure reason, check if it is as expected
            Assert.equal(reason, 'Invalid class', 'failed with unexpected reason');
        } catch (bytes memory /*lowLevelData*/) {
            Assert.ok(false, 'failed unexpected');
        }
    }
    
    /// Test another failure case of 'add' using try-catch
    function testAddFailureUsingTryCatch2() public {
        // This will revert on 'require(register[rollNumber].class == 0, "Roll number not available");' for rollNumber '101'
        try ar.add(101, 'secondStudent', 11) returns (uint256 r) {
            Assert.ok(false, 'method execution should fail');
        } catch Error(string memory reason) {
            // Compare failure reason, check if it is as expected
            Assert.equal(reason, 'Roll number not available', 'failed with unexpected reason');
        } catch (bytes memory /*lowLevelData*/) {
            Assert.ok(false, 'failed unexpected');
        }
    }
    
    /// For solidity version less than 0.6.0, low level call can be used
    /// See: https://solidity.readthedocs.io/en/v0.6.0/units-and-global-variables.html#members-of-address-types
    /// Test success case of 'add' using low level call
    function testAddSuccessUsingCall() public {
        bytes memory methodSign = abi.encodeWithSignature('add(uint256,string,uint256)', 102, 'firstStudent', 10);
        (bool success, bytes memory data) = address(ar).call(methodSign);
        // 'success' stores the result in bool, this can be used to check whether method call was successful
        Assert.equal(success, true, 'execution should be successful');
        // 'data' stores the returned data which can be decoded to get the actual result
        uint rollNumber = abi.decode(data, (uint256));
        // check if result is as expected
        Assert.equal(rollNumber, 102, 'wrong rollNumber');
    }
    
    /// Test failure case of 'add' using low level call
    function testAddFailureUsingCall() public {
        bytes memory methodSign = abi.encodeWithSignature('add(uint256,string,uint256)', 102, 'duplicate', 10);
        (bool success, bytes memory data) = address(ar).call(methodSign);
        // 'success' will be false if method execution is not successful
        Assert.equal(success, false, 'execution should be successful');
    }
}

4.测试一个包含msg.value的方法`

在Solidity中,ether可以随方法调用一起传递,并在合约内作为msg.value访问。有时,方法中的多个计算都是基于msg.value进行的,可以使用 Remix 的自定义交易环境测试不同的值。见以下示例:

要测试的合约/程序:Value.sol

pragma solidity >=0.4.22 <0.7.0;
contract Value {
    uint256 public tokenBalance;
    
    constructor() public {
        tokenBalance = 0;
    }
    
    function addValue() payable public {
        tokenBalance = tokenBalance + (msg.value/10);
    } 
    
    function getTokenBalance() view public returns (uint256) {
        return tokenBalance;
    }
}

测试合约/程序:Value_test.sol

pragma solidity >=0.4.22 <0.7.0;
import "remix_tests.sol"; 
import "./Value.sol";

contract ValueTest{
    Value v;
    
    function beforeAll() public {
        // create a new instance of Value contract
        v = new Value();
    }
    
    /// Test initial balance
    function testInitialBalance() public {
        // initially token balance should be 0
        Assert.equal(v.getTokenBalance(), 0, 'token balance should be 0 initially');
    }
    
    /// For Solidity version greater than 0.6.1
    /// Test 'addValue' execution by passing custom ether amount 
    /// #value: 200
    function addValueOnce() public payable {
        // check if value is same as provided through devdoc
        Assert.equal(msg.value, 200, 'value should be 200');
        // execute 'addValue'
        v.addValue{gas: 40000, value: 200}(); // introduced in Solidity version 0.6.2
        // As per the calculation, check the total balance
        Assert.equal(v.getTokenBalance(), 20, 'token balance should be 20');
    }
    
    /// For Solidity version less than 0.6.2
    /// Test 'addValue' execution by passing custom ether amount again using low level call
    /// #value: 100
    function addValueAgain() public payable {
        Assert.equal(msg.value, 100, 'value should be 100');
        bytes memory methodSign = abi.encodeWithSignature('addValue()');
        (bool success, bytes memory data) = address(v).call.gas(40000).value(100)(methodSign);
        Assert.equal(success, true, 'execution should be successful');
        Assert.equal(v.getTokenBalance(), 30, 'token balance should be 30');
    }
}

5. Testing a method involving msg.sender and msg.value

在以下测试中,我们将模拟多个帐户向智能合约中的同一收件人进行存款,最后让收件人取回所有捐款的总和。我们还验证了捐款金额是否与预期金额匹配。这个示例说明了如何在使用一组不同的msg.value时在不同账户间切换。

要测试的合约/程序:Donations.sol

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;

contract donations{ 
    struct Donation {
        uint id;
        uint amount;
        string donor;
        string message;
        uint timestamp; //seconds since unix start
    }
    uint amount = 0;
    uint id = 0;
    mapping(address => uint) public balances;
    mapping(address => Donation[]) public donationsMap;

    function donate(address _recipient, string memory _donor, string memory _msg) public payable {
        require(msg.value > 0, "The donation needs to be >0 in order for it to go through");
        amount = msg.value;
        balances[_recipient] += amount;        
        donationsMap[_recipient].push(Donation(id++,amount,_donor,_msg,block.timestamp));
    }

    function withdraw() public {  //whole thing by default.
        amount = balances[msg.sender];
        balances[msg.sender] -= amount;
        require(amount > 0, "Your current balance is 0");
        (bool success,) = msg.sender.call{value:amount}("");
        if(!success){
            revert();
        }
    }
  
    function balances_getter(address _recipient) public view returns (uint){
            return balances[_recipient];
    }
    
    function getBalance() public view returns(uint) {
            return msg.sender.balance;
    }
}

测试合约/程序:Donations_test.sol

// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.4.22 <0.9.0;
import "remix_tests.sol"; 
import "remix_accounts.sol";
import "../donations.sol";

contract testSuite is donations {
    address acc0 = TestsAccounts.getAccount(0); //owner by default
    address acc1 = TestsAccounts.getAccount(1);
    address acc2 = TestsAccounts.getAccount(2);
    address acc3 = TestsAccounts.getAccount(3);
    address recipient = TestsAccounts.getAccount(4); //recipient

    /// #value: 1000000000000000000
    /// #sender: account-1
    function donateAcc1AndCheckBalance() public payable{
        Assert.equal(msg.value, 1000000000000000000, 'value should be 1 Eth');
        donate(recipient, "Mario", "Are you a bird?");
        Assert.equal(balances_getter(recipient), 1000000000000000000, 'balances should be 1 Eth');
    }
    
    /// #value: 1000000000000000000
    /// #sender: account-2
    function donateAcc2AndCheckBalance() public payable{
        Assert.equal(msg.value, 1000000000000000000, 'value should be 1 Eth');
        donate(recipient, "Tom", "Are you a plane?");
        Assert.equal(balances_getter(recipient), 2000000000000000000, 'balances should be 2 Eth');
    }
    
    /// #value: 2000000000000000000
    /// #sender: account-3
    function donateAcc3AndCheckBalance() public payable{
        Assert.equal(msg.value, 2000000000000000000, 'value should be 1 Eth');
        donate(recipient, "Maria", "Are you a car?");
        Assert.equal(balances_getter(recipient), 4000000000000000000, 'balances should be 4 Eth');
    }
    
    /// #sender: account-4
    function withdrawDonations() public payable{
        uint initialBal = getBalance();
        withdraw();
        uint finalBal = getBalance();
        Assert.equal(finalBal-initialBal, 4000000000000000000, 'balances should be 4 Eth');
    }
}