如何使用 Solidity 和 JavaScript 测试智能合约【译】

Truffle 框架支持 Solidity和 JavaScript 编写测试用例,本文介绍了他们的区别与应用场景。

阅读本文需要对区块链,[以太坊](https://learnblockchain.cn/categories/ethereum/),JavaScript 有所了解。 *所有的代码可以在* [*Github* ](https://github.com/alexroan/truffle-tests-tutorial) ## 软件测试的重要性 如果您希望代码按照预期的方式工作,那么软件测试至关重要。 软件测试有两种常规类型:*单元测试*和*集成测试*。 - **单元测试**地关注每个独立的功能。 - **集成测试**重点在于确保代码的多个部分按预期在一起工作。 区块链软件也不例外。 而且由于[不可变性](https://en.wikipedia.org/wiki/Immutable_object),区块链应用程序需要更多地强调测试。 ## 区块链测试 Truffle 开发框架为我们提供了两种测试[Solidity](https://learnblockchain.cn/docs/solidity/)智能合约的途径:Solidity测试和JavaScript测试。 问题是,我们应该使用哪个? 答案是都需要。 ![Figure 1: Test structure diagram](https://img.learnblockchain.cn/2020/04/12/R6LFUQ.png) ### Solidity 测试 用Solidity编写智能合约的测试用例让我们可以在区块链层级进行测试。这种测试用例可以调用合约方法,就像用例部署在区块链里一样。为了测试智能合约的内部行为,我们可以: - 编写Solidity**单元测试**来检查智能合约函数的返回值以及状态变量的值。 - 编写Solidity**集成测试**来检查智能合约之间的交互。这些集成测试可以确保像继承或者依赖注入这样的机制的运行符合预期 ### JavaScript 测试 我们也需要确保智能合约能够表现出正确的外部行为。为了从区块链外部测试智能合约,我们在JavaScript测试用例中使用web3.js,就像在 开发DApp时一样。我们需要有信心对DApp前端可以正确调用智能合约。 这方面的测试属于集成测试。 ## 示例项目 > [项目的网站代码 Github](https://github.com/alexroan/truffle-tests-tutorial) 我们有两个合约: `Background `and `EntryPoint `需要测试. `Background ` 是一个内部合约,DApp前端不会直接和它交互。`EntryPoint ` 则是设计作为供DApp交互的智能合约,在`EntryPoint`合约会引用`Background`合约。 ### 合约 Background合约代码如下: ```javascript 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 合约代码如下: ```javascript 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` 合约的部署地址,并将其存入一个状态变量`backgroundAddress`。`EntryPoint`合约暴露出三个函数: - `getBackgroundAddress()`:返回Background合约的部署地址 - `storeTwoValues(uint, uint)`:保存两个值 - `getNumberOfValues()`:返回值的数量 `storeTwoValues(uint, uint) `函数调用两次`Background`合约中的函数,因此对这个函数进行独立单元测试比较困难。`getNumberOfValues()`也有同样的问题,因此这两个函数更适合进行集成测试。 ### Solidity 测试用例 在 Solidity 测试用例中,我们将为智能合约编写Solidity单元测试用例和集成测试用例。 让我们先从简单一点的单元测试开始。 `TestBackground ` 测试用例如下: ```javascript 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`合约的功能符合预期: ```javascript 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`合约交互,因此我们没有办法单独测试这些函数,需要在集成测试中进行验证。下面是集成测试的代码: ```javascript 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`: ```javascript 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级别的集成测试。 该项目中有些地方可能要编写更多的单元或集成测试,因此,如果您认为可以添加到该项目中,请向[代码库](https://github.com/alexroan/truffle-tests-tutorial) 提交 Pull request。 ## 进一步阅读 如果你觉得本文对你用户,下面的文章也可能对你有用: - [了解如何使用Truffle将智能合约部署到公共测试网络(英文) ](https://medium.com/swlh/develop-test-and-deploy-your-first-ethereum-smart-contract-with-truffle-14e8956d69fc). > 这里有一篇中文文章介绍[使用Truffle将智能合约部署到公共测试网络 ](https://learnblockchain.cn/2019/03/30/dapp_noteOnChain). - 确保智能合约免受黑客攻击,了解 [可重入攻击和所有者逻辑盗窃攻击以及如何防止它们 ](https://medium.com/coinmonks/how-to-secure-your-smart-contracts-dc500f2c8fca). - [了解如何正确加载区块链,以便您的DApp提供出色的用户体验 ](https://medium.com/better-programming/ethereum-dapps-how-to-load-the-blockchain-8756ca0fa0d1). 原文链接:https://medium.com/better-programming/how-to-test-ethereum-smart-contracts-35abc8fa199d

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

所有的代码可以在 Github

软件测试的重要性

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

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

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

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

区块链测试

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

答案是都需要。

如何使用 Solidity 和 JavaScript 测试智能合约【译】插图

Solidity 测试

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

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

JavaScript 测试

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

示例项目

项目的网站代码 Github

我们有两个合约: Backgroundand 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 &lt; 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。

进一步阅读

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

  • 了解如何使用Truffle将智能合约部署到公共测试网络(英文) .

    这里有一篇中文文章介绍使用Truffle将智能合约部署到公共测试网络 .

  • 确保智能合约免受黑客攻击,了解 可重入攻击和所有者逻辑盗窃攻击以及如何防止它们 .

  • 了解如何正确加载区块链,以便您的DApp提供出色的用户体验 .

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

区块链技术网。

  • 发表于 2020-04-12 22:02
  • 阅读 ( 2803 )
  • 学分 ( 189 )
  • 分类:以太坊

评论