13

如何使用 Solidity 和 JavaScript 测试智能合约【译】 | 登链社区 | 深入浅出区块链技...

 4 years ago
source link: https://learnblockchain.cn/article/859?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

阅读本文需要对区块链,以太坊,JavaScript 有所了解。

所有的代码可以在 Github

软件测试的重要性

如果您希望代码按照预期的方式工作,那么软件测试至关重要。

软件测试有两种常规类型:单元测试集成测试

  • 单元测试地关注每个独立的功能。
  • 集成测试重点在于确保代码的多个部分按预期在一起工作。

区块链软件也不例外。 而且由于不可变性,区块链应用程序需要更多地强调测试。

区块链测试

Truffle 开发框架为我们提供了两种测试 Solidity 智能合约的途径:Solidity 测试和 JavaScript 测试。 问题是,我们应该使用哪个?

答案是都需要。

Figure 1: Test structure diagram

Solidity 测试

用 Solidity 编写智能合约的测试用例让我们可以在区块链层级进行测试。这种测试用例可以调用合约方法,就像用例部署在区块链里一样。为了测试智能合约的内部行为,我们可以:

  • 编写 Solidity 单元测试来检查智能合约函数的返回值以及状态变量的值。
  • 编写 Solidity 集成测试来检查智能合约之间的交互。这些集成测试可以确保像继承或者依赖注入这样的机制的运行符合预期

JavaScript 测试

我们也需要确保智能合约能够表现出正确的外部行为。为了从区块链外部测试智能合约,我们在 JavaScript 测试用例中使用 web3.js,就像在 开发 DApp 时一样。我们需要有信心对 DApp 前端可以正确调用智能合约。 这方面的测试属于集成测试。

项目的网站代码 Github

我们有两个合约: Background and EntryPoint 需要测试。

Background 是一个内部合约,DApp 前端不会直接和它交互。EntryPoint 则是设计作为供 DApp 交互的智能合约,在 EntryPoint 合约会引用 Background 合约。

Background 合约代码如下:

pragma solidity >=0.5.0;

contract Background {
    uint[] private values;

    function storeValue(uint value) public {
        values.push(value);
    }
    
    function getValue(uint initial) public view returns(uint) {
        return values[initial];
    }
    
    function getNumberOfValues() public view returns(uint) {
        return values.length;
    }
}

在上面,我们看到 Background 合约提供了三个函数:

  • storeValue(uint):存值
  • getValue(uint) :读取值
  • getNumberOfValues():获取值的

这三个合约函数都很简单,因此也很容易进行单元测试。

EntryPoint.sol 合约代码如下:

pragma solidity >=0.5.0;

import "./Background.sol";

contract EntryPoint {
    address public backgroundAddress;

    constructor(address _background) public{
        backgroundAddress = _background;
    }

    function getBackgroundAddress() public view returns (address) {
        return backgroundAddress;
    }

    function storeTwoValues(uint first, uint second) public {
        Background(backgroundAddress).storeValue(first);
        Background(backgroundAddress).storeValue(second);
    }

    function getNumberOfValues() public view returns (uint) {
        return Background(backgroundAddress).getNumberOfValues();
    }
}

EntryPoint 合约的构造函数中,使用了 Background 合约的部署地址,并将其存入一个状态变量 backgroundAddressEntryPoint 合约暴露出三个函数:

  • getBackgroundAddress():返回 Background 合约的部署地址
  • storeTwoValues(uint, uint):保存两个值
  • getNumberOfValues():返回值的数量

storeTwoValues(uint, uint) 函数调用两次 Background 合约中的函数,因此对这个函数进行独立单元测试比较困难。getNumberOfValues() 也有同样的问题,因此这两个函数更适合进行集成测试。

Solidity 测试用例

在 Solidity 测试用例中,我们将为智能合约编写 Solidity 单元测试用例和集成测试用例。 让我们先从简单一点的单元测试开始。

TestBackground 测试用例如下:

pragma solidity >=0.5.0;

import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../../../contracts/Background.sol";

contract TestBackground {

    Background public background;

    // 在每个测试函数之前运行
    function beforeEach() public {
        background = new Background();
    }

    // Test that it stores a value correctly
    function testItStoresAValue() public {
        uint value = 5;
        background.storeValue(value);
        uint result = background.getValue(0);
        Assert.equal(result, value, "It should store the correct value");
    }

    // Test that it gets the correct number of values
    function testItGetsCorrectNumberOfValues() public {
        background.storeValue(99);
        uint newSize = background.getNumberOfValues();
        Assert.equal(newSize, 1, "It should increase the size");
    }

    // Test that it stores multiple values correctly
    function testItStoresMultipleValues() public {
        for (uint8 i = 0; i < 10; i++) {
            uint value = i;
            background.storeValue(value);
            uint result = background.getValue(i);
            Assert.equal(result, value, "It should store the correct value for multiple values");
        }
    }
}

它测试了 Background 合约,确保它:

  • values 数组中保存新的值
  • 按索引返回 values
  • values 数组中保存多个值
  • 返回 values 数组的大小

下面是 TestEntryPoint , 包含了一个单元测试 testItHasCorrectBackground() 用于验证 EntryPoint 合约的功能符合预期:

pragma solidity >=0.5.0;

import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../../../contracts/Background.sol";
import "../../../contracts/EntryPoint.sol";

contract TestEntryPoint {

    // Ensure that dependency injection working correctly
    function testItHasCorrectBackground() public {
        Background backgroundTest = new Background();
        EntryPoint entryPoint = new EntryPoint(address(backgroundTest));
        address expected = address(backgroundTest);
        address target = entryPoint.getBackgroundAddress();
        Assert.equal(target, expected, "It should set the correct background");
    }

}

这个函数测试了注入的依赖。如前所述,EntryPoint 合约中的其他函数需要与 Background 合约交互,因此我们没有办法单独测试这些函数,需要在集成测试中进行验证。下面是集成测试的代码:

pragma solidity >=0.5.0;

import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../../../contracts/Background.sol";
import "../../../contracts/EntryPoint.sol";

contract TestIntegrationEntryPoint {

    BackgroundTest public backgroundTest;
    EntryPoint public entryPoint;

    // 在测试用例之前运行
    function beforeEach() public {
        backgroundTest = new BackgroundTest();
        entryPoint = new EntryPoint(address(backgroundTest));
    }

    // Check that storeTwoValues() works correctly.
    // EntryPoint contract should call background.storeValue()
    // so we use our mock extension BackgroundTest contract to
    // check that the integration workds
    function testItStoresTwoValues() public {
        uint value1 = 5;
        uint value2 = 20;
        entryPoint.storeTwoValues(value1, value2);
        uint result1 = backgroundTest.values(0);
        uint result2 = backgroundTest.values(1);
        Assert.equal(result1, value1, "Value 1 should be correct");
        Assert.equal(result2, value2, "Value 2 should be correct");
    }

    // Check that entry point calls our mock extension correctly
    // indicating that the integration between contracts is working
    function testItCallsGetNumberOfValuesFromBackground() public {
        uint result = entryPoint.getNumberOfValues();
        Assert.equal(result, 999, "It should call getNumberOfValues");
    }
}

// Extended from Background because values is private in actual Background
// but we're not testing background in this unit test
contract BackgroundTest is Background {
    uint[] public values;

    function storeValue(uint value) public {
        values.push(value);
    }

    function getNumberOfValues() public view returns(uint) {
        return 999;
    }
}

我们可以看到 TestIntegrationEntryPoint 使用了一个 Background 的扩展,即定义在第 43 行的 BackgroundTest,以其作为我们的模拟合约,这可以让我们的测试用例检查 EntryPoint 函数是否调用了部署在 backgroundAddress 地址处的合约。

JavaScript 测试文件

用 JavaScript 编写集成测试来确保合约的外部行为满足预期要求,这样我们就可以基于这些智能合约开发 DApp 了。

下面是我们的 JavaScript 测试文件 entryPoint.test.js

const EntryPoint = artifacts.require("./EntryPoint.sol");

require('chai')
    .use(require('chai-as-promised'))
    .should();

contract("EntryPoint", accounts => {
    describe("Storing Values", () => {
        it("Stores correctly", async () => {
            const entryPoint = await EntryPoint.deployed();

            let numberOfValues = await entryPoint.getNumberOfValues();
            numberOfValues.toString().should.equal("0");

            await entryPoint.storeTwoValues(2,4);
            numberOfValues = await entryPoint.getNumberOfValues();
            numberOfValues.toString().should.equal("2");
        });
    });
});

使用 EntryPoint 合约中的函数,JavaScript 测试用例可以将区块链外部的值通过交易传入智能合约,这是通过调用合约的 storeTwoValues(uint,uint) 函数(第 15 行)实现的。

通过在测试的第 12 行和第 16 行调用 getNumberOfValues() 来检索存储在区块链中的值的数量,以确保和存储的值一致。

在测试智能合约时,越多越好。 应该不遗余力确保所有可能的执行路径返回预期结果。 将链级 Solidity 测试用于单元测试和集成测试,并将 JavaScript 测试用于 DApp 级别的集成测试。

该项目中有些地方可能要编写更多的单元或集成测试,因此,如果您认为可以添加到该项目中,请向代码库 提交 Pull request。

进一步阅读

如果你觉得本文对你用户,下面的文章也可能对你有用:

原文链接:https://medium.com/better-programming/how-to-test-ethereum-smart-contracts-35abc8fa199d


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK