以太坊智能合約測試的兩種方法【Solidity

Truffle開發框架提供了以太坊智能合約測試的兩種方法:區塊鏈級別的 Solidity測試和DApp級別的JavaScript測試。在這個教程中,我們將介紹 這兩種以太坊智能合約測試方法的用途、區別與應用場景,並通過一個具體的示例 來學習如何綜合利用Solitiy測試用例和JavaScript測試用例對以太坊智能 合約進行單元測試和集成測試。

七種開發語言的以太坊教程: Java | Php | Python | .Net / C# | Golang | Node.JS | Flutter / Dart

1、以太坊智能合約測試概述

作為軟件開發者,我們都知道要讓代碼正常運行,測試是非常 重要的一個環節。基於區塊鏈的去中心化軟件也不例外,而且由於區塊鏈的 不可修改特性,測試對於區塊鏈軟件來說就更重要了。

總體上來說有兩種類型的軟件測試:單元測試和集成測試。單元測試 聚焦於單個函數的測試,而集成測試的目標則是確保各部分代碼 組合起來也可以按期望的方式運行。

Truffle是應用最廣的以太坊智能合約與DApp開發框架,它提供了兩種用於 測試以太坊智能合約的方法:Solidity測試和JavaScript測試。問題是, 我們應該選擇哪一種方法?

答案是都需要。

以太坊智能合約測試的兩種方法【Solidity | JavaScript】

用Solidity編寫智能合約的測試用例讓我們可以在區塊鏈層級進行測試。這種測試 用例可以調用合約方法,就像用例部署在區塊鏈裡一樣。為了測試智能合約的內部 行為,我們可以:

  • 編寫Solidity單元測試來檢查智能合約函數的返回值以及狀態變量的值
  • 編寫Solidity集成測試來檢查智能合約之間的交互。這些集成測試可以確保像繼承或者 依賴注入這樣的機制的運行符合預期

我們也需要確保智能合約能夠表現出正確的外部行為。為了從區塊鏈 外部測試智能合約,我們在JavaScript測試用例中使用web3.js,就像在 開發DApp時一樣。我們需要對DApp前端可以正確調用智能合約建立信心。 這方面的測試屬於集成測試。

因此,簡單地說,Solidity測試用例主要用於智能合約內部實現邏輯的驗證, 可以用於單元測試和集成測試;而JavaScript用例則主要用於智能合約外部行為 的驗證,通常用於集成測試。

2、以太坊智能合約測試的示例項目

假設我們有兩個以太坊智能合約需要測試:Background和Entrypoint。

Background是一個內部合約,我們的DApp前端不會直接和它交互。EntryPoint 則是專門供DApp交互的智能合約,在EntryPoint合約內部會訪問Background合約。

下面是Background合約的solidity代碼:

<code>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;
}
}/<code>

在上面,我們看到Background合約提供了三個函數:

  • storeValue(uint):寫入值
  • getValue(uint) :讀取值
  • getNumberOfValues():獲取值的數量

這三個合約函數都很簡單,因此也很容易進行單元測試。

下面是EntryPoint合約的Solidity代碼:

<code>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();
}
}/<code>

在EntryPoint合約的構造函數中,我們注入了Background合約的部署地址,並 將其存入一個狀態變量backgroundAddress。EntryPoint合約暴露出三個函數:

  • getBackgroundAddress():返回Background合約的部署地址
  • storeTwoValues(uint, uint):保存兩個值
  • getNumberOfValues():返回值的數量

由於storeTwoValues(uint, uint)函數兩次調用Background合約中的一個函數,因此 對這個函數進行單元測試比較困難。getNumberOfValues()也有同樣的問題,因此這 兩個函數更適合進行集成測試。

3、以太坊智能合約測試的Solidity用例

在這一部分,我們學習如何為智能合約編寫Solidity單元測試用例和集成測試用例。 讓我們先從簡單一點的單元測試開始。

下面是TestBackground測試的代碼:

<code>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");
}
}
}/<code>

這個單元測試的目的是確保Background合約可以:

  • 在values數組中保存新的值
  • 按索引返回values
  • 在values數組中保存多個值
  • 返回values數組的大小

下面的TestEntryPoint測試中包含了一個單元測試testItHasCorrectBackground() 用於驗證EntryPoint合約的功能符合預期:

<code>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");
}

}/<code>

這個函數對依賴注入進行測試。如前所述,EntryPoint合約中的其他函數需要 與Background合約交互,因此我們沒有辦法單獨測試這些函數,需要在集成測試 中進行驗證。下面是集成測試的代碼:

<code>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;
}
}/<code>

我們可以看到TestIntegrationEntryPoint使用了一個Background的擴展,即定義在第43行的 BackgroundTest,以其作為我們的模擬合約,這可以讓我們的測試用例檢查EntryPoint 是否調用了部署在backgroundAddress地址處的合約的正確的函數。

4、以太坊智能合約測試的JavaScript用例

我們用JavaScript編寫集成測試來確保合約的外部行為滿足預期要求,這樣我們 就有信息基於這些智能合約開發DApp了。

下面是我們的JavaScript測試文件entryPoint.test.js:

<code>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");
});
});
});/<code>

使用EntryPoint合約中的函數,JavaScript測試可以確保我們可以將 區塊鏈外部的值利用交易傳入智能合約,這是通過調用合約的storeTwoValues(uint,uint) 函數(第15行)實現的。

5、以太坊智能合約測試教程小節

當談到智能合約的測試時,可以說越多越好,應當儘可能覆蓋所有可能的 執行路徑都返回預期的結果。Truffle提供了兩種辦法:區塊鏈層的Solidity單元測試 和集成測試,以及DApp級別的JavaScript集成測試,我們在實際的工作中需要 根據智能合約的代碼實現綜合運用這兩種測試方法來保證智能合約的運行符合預期。


原文鏈接:http://blog.hubwiz.com/2020/03/25/solidity-test-tutorial/


分享到:


相關文章: