实战以太坊智能合约测试【Truffle】

简介:

Truffle开发框架提供了以太坊智能合约测试的两种方法:区块链级别的Solidity测试和DApp级别的JavaScript测试。在这个教程中,我们将介绍这两种以太坊智能合约测试方法的用途、区别与应用场景,并通过一个具体的示例来学习如何综合利用Solitiy测试用例和JavaScript测试用例对以太坊智能
合约进行单元测试和集成测试。

七种开发语言的以太坊教程:Java | Php | Python | .Net / C# | Golang | Node.JS | Flutter / Dart

1、以太坊智能合约测试概述

作为软件开发者,我们都知道要让代码正常运行,测试是非常重要的一个环节。基于区块链的去中心化软件也不例外,而且由于区块链的不可修改特性,测试对于区块链软件来说就更重要了。

总体上来说有两种类型的软件测试:单元测试和集成测试。单元测试聚焦于单个函数的测试,而集成测试的目标则是确保各部分代码组合起来也可以按期望的方式运行。

Truffle是应用最广的以太坊智能合约与DApp开发框架,它提供了两种用于测试以太坊智能合约的方法:Solidity测试和JavaScript测试。问题是,我们应该选择哪一种方法?

答案是都需要。

在这里插入图片描述

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

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

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

因此,简单地说,Solidity测试用例主要用于智能合约内部实现逻辑的验证,可以用于单元测试和集成测试;而JavaScript用例则主要用于智能合约外部行为的验证,通常用于集成测试。

2、以太坊智能合约测试的示例项目

假设我们有两个以太坊智能合约需要测试:Background和Entrypoint。

Background是一个内部合约,我们的DApp前端不会直接和它交互。EntryPoint则是专门供DApp交互的智能合约,在EntryPoint合约内部会访问Background合约。

下面是Background合约的solidity代码:

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合约的Solidity代码:

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()也有同样的问题,因此这两个函数更适合进行集成测试。

3、以太坊智能合约测试的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;

    // Run before every test function
    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;

    // Run before every test function
    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地址处的合约的正确的函数。

4、以太坊智能合约测试的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行)实现的。

5、以太坊智能合约测试教程小节

当谈到智能合约的测试时,可以说越多越好,应当尽可能覆盖所有可能的执行路径都返回预期的结果。Truffle提供了两种办法:区块链层的Solidity单元测试和集成测试,以及DApp级别的JavaScript集成测试,我们在实际的工作中需要根据智能合约的代码实现综合运用这两种测试方法来保证智能合约的运行符合预期。


原文链接:以太坊智能合约测试的两种方法 — 汇智网

目录
相关文章
|
2月前
|
Java 测试技术 Maven
JAVA单元测试概念与实战
单元测试是软件开发中的一个测试方法,用于验证软件代码中最小的、独立的单元是否按照预期工作。在Java中,这通常指的是单个的方法或者一个类的个别功能。单元测试的目的是隔离代码的每个部分,并确保各个部分是正确的。
51 4
|
7月前
|
Shell 测试技术
Shell编程实战的命令测试
Shell编程实战的命令测试
37 1
|
3月前
|
监控 数据可视化 Java
jvm性能调优实战 - 31从测试到上线_如何分析JVM运行状况及合理优化
jvm性能调优实战 - 31从测试到上线_如何分析JVM运行状况及合理优化
51 1
|
4月前
|
机器学习/深度学习 人工智能 算法
软件测试/人工智能|人工智能与自动化测试结合实战-探索人工智能在测试领域中的应用
软件测试/人工智能|人工智能与自动化测试结合实战-探索人工智能在测试领域中的应用
143 0
|
1月前
|
运维 数据库
Powershell实战:测试网络请求两个命令介绍
【2月更文挑战第11篇】 Test-Connection 命令将 Internet 控制消息协议 (ICMP) 回显请求数据包或 ping 发送给一台或多台远程计算机并返回回显响应回复。 我们可以使用该命令确定是否可通过 IP 网络ping通特定的计算机。
|
2月前
|
测试技术 API Python
Python自动化测试:unittest与pytest的实战技巧
Python自动化测试:unittest与pytest的实战技巧
|
7月前
|
数据采集 算法 测试技术
深聊性能测试,从入门到放弃之:Locust性能自动化(二)代码实战
深聊性能测试,从入门到放弃之:Locust性能自动化(二)代码实战
156 1
|
4月前
|
缓存 Java 关系型数据库
Spring Boot实现RESTful接口架构实战(包括REST的讲解、定义、REST服务测试)
Spring Boot实现RESTful接口架构实战(包括REST的讲解、定义、REST服务测试)
52 0
|
7月前
|
NoSQL 测试技术 API
从程序员到架构师开发运维场景实战篇:一人一套测试环境
一人一套测试环境 本篇开始讲第16次架构经历:一人一套测试环境。同样,先介绍业务场景。 业务场景:测试环境何时能释放出来使用 当时,公司的基础设施使用的是虚拟机,而且还未迁移到容器。
|
8月前
|
API Android开发 数据安全/隐私保护
《协议测试》抓包工具Fiddler实战教程 2
《协议测试》抓包工具Fiddler实战教程