iT邦幫忙

2022 iThome 鐵人賽

DAY 16
1
Web 3

從以太坊白皮書理解 web 3 概念系列 第 17

從以太坊白皮書理解 web 3 概念 - Day16

  • 分享至 

  • xImage
  •  

從以太坊白皮書理解 web 3 概念 - Day16

Learn Solidity - Day 8 - Testing Smart Contracts with Truffle

前面講述關於 Smart Contract 的撰寫

今天將會透過 Lession 11 - Testing Smart Contracts with Truffle 來學關於如何測試 Smart Contract

因為一旦發佈到鏈上, bug 將會永遠存在鏈上

所以最好的方式就是在上鏈之前就作法測試

這章節將使用 Truffle 這個測試工具

並且使用本地節點 Gancache 在本機架設 Ethereum 節點

還有透過 Chai 撰寫測試

環境設定

假設以前幾章節的 Project ZombieContract

其在 Truffle 的架構下

專案檔案架構應該如下

├── build
  ├── contracts
      ├── Migrations.json
      ├── CryptoZombies.json
      ├── erc721.json
      ├── ownable.json
      ├── safemath.json
      ├── zombieattack.json
      ├── zombiefactory.json
      ├── zombiefeeding.json
      ├── zombiehelper.json
      ├── zombieownership.json
├── contracts
  ├── Migrations.sol
  ├── CryptoZombies.sol
  ├── erc721.sol
  ├── ownable.sol
  ├── safemath.sol
  ├── zombieattack.sol
  ├── zombiefactory.sol
  ├── zombiefeeding.sol
  ├── zombiehelper.sol
  ├── zombieownership.sol
├── migrations
└── test
. package-lock.json
. truffle-config.js
. truffle.js

實作測試檔案

建立一個 CryptoZombies.js 在 test 資料下

touch test/CryptoZombies.js

建立 artifacts

每次當編譯一個 Smart Contract, solidity 編譯器會產生一個 JSON 檔案。這個 JSON 檔案包含 Contract abi 會存放在 build/contracts 資料夾下

當 Contract 更新執行 migration 時, Truffle 會自動更新 build/contracts 資料夾內的內容

而測試第一件事情就是載入 build/contracts 內建立好的 artifacts 來做測試。

語法如下:

const MyAwesomeContract = artifacts.require("MyAwesomeContract");

這個 function 會讀出 contract abi 。讓 javascript 方便可以透過該 interface 與 Contract 做互動

contract() function

在背後, Truffle 有使用 Mocha 這個測試的框架為了簡化測試。

以下會是接下會去實作的部份:

  • group test: 透過呼叫 contract() function 來做群組測試 。 contract function 繼承了 Mocha 的 describe 功能,加入了一組 account 測試並且再測試完會自動清除。
    contract() 需要兩個參數:第一個是 string 用來描素要做的測項,第二個是 callback 是真正要去執行測試的部份
  • 執行: 透過 it() function 可以真正去執行測試。
    it() function 需要兩個參數,第一個是字串用來描述測試主題,第二個是 callback 會真正實作測試本身。

範例如下:

contract("MyAwesomeContract", (accounts) => {
	it("should be able to receive Ethers", () => {
	})
})

撰寫測試

  1. 宣告 const CryptoZombies 變數,設定其值為 aritifacts.require 要測試的 contract
  2. 宣告 contract() 並寫入測試大項內容
  3. 宣告 it() 並寫入測試大項內容
const CryptoZombies = artifacts.require("CryptoZombies");

contract("CryptoZombies", (accounts) => {
    it("should be able to create a new zombie", () => {

    })
})

第一個測試 -- 建立一個新的 Zombie

在發佈到 Ethereum 鏈上前,可以先在自己架設的 Ganache 節點測試一下。

每次開啟 Gancache 節點時,Gancache 會自動建立 10 個測試帳號,每個初始化 100 Ether。

因為 Truffle 與 Gancache 整合的很好,所以可以直接透過 accounts 這個陣列直接取得帳號的 address

語法如下

let [alice, bob] = accounts;

回顧建立 Zombie Contract 的部份

function createRandomZombie(string _name) public {
   require(ownerZombieCount[msg.sender] == 0);
   uint randDna = _generateRandomDna(_name);
   randDna = randDna - randDna % 100;
   _createZombie(_name, randDna);
 }

撰寫測試

  1. 在 contract(), 宣告兩個變數 alice 與 bob 從 accounts 讀取出值。
  2. 添加 async 宣告到 it() 的第二個 callback function 前面,為了比較好撰寫非同步邏輯
const CryptoZombies = artifacts.require("CryptoZombies");
contract("CryptoZombies", (accounts) => {
    //1. initialize `alice` and `bob`
	let [alice, bob] = accounts;
    it("should be able to create a new zombie", async () => { //2 & 3. Replace the first parameter and make the callback async
    })
})

撰寫 Create New Zombie 測試邏輯

測試邏輯可以分成3個步驟

  1. 設定:這步驟是定一個測試前的狀態,初始化一些設定還有參數
  2. 執行:根據設定的參數實際上去執行撰寫的邏輯。通常我們會把測試範圍儘量縮小。
  3. 檢驗:檢驗執行步驟所得到的結果

舉個例子:

假設要測試 MyAwesomeContract 的建立新 createRandomZombie 功能

因為還需要傳入 zombie 名稱來呼叫

設定的部份會如下:

const contractInstance = await MyAwesomeContract.new();
//
const zombieNames = ["Zombie #1", "Zombie #2"];

執行的部份則會如下

contractInstance.createRandomZombie(zombieNames[0]);

撰寫測試設定部份

  1. 宣告 const contractInstance = await CryptoZombies.new();
const CryptoZombies = artifacts.require("CryptoZombies");
const zombieNames = ["Zombie 1", "Zombie 2"];
contract("CryptoZombies", (accounts) => {
    let [alice, bob] = accounts;
    it("should be able to create a new zombie", async () => {
        // start here
		const constractInstance = await CryptoZombies.new();
    })
})

撰寫 Creating a New Zombie 測試執行的部份

執行 createRandomZombie

面臨的第一個問題是? 如何設定 ZombieOwner

透過 abi ,可以透過帶入 from 參數設定 owner

如下:

const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});

Logs and Events

第二個問題是 result 的格式是什麼?

在 Truffle 從 artifacts.require 初始化 abi 後,

Truffle 會自動透過 Smart Contract 產生 log 。

舉例來說:
result.logs[0].args.name 就可以取的執行後的 zombie 名稱。

此外基上 , result 還有其他重要的結構如下

  • result.tx: 產生的 transaction hash
  • result.receipt: 一個物件包含 transaction 的 receipt 物件。 如果 result.receipt.status 是 true , 代表交易成功,否則就是失敗。

驗證

驗證的部份可以透過 assertion function 如 equal 或是 deepEqual 來做。但這邊只會查單純的值,所以只會用 assert.equal()

實作步驟

  1. 宣告 const result 並且設定值為執行contractInstance.createRandomZombie 的結果
  2. 加入以下檢驗 assert.equal(result.receipt.status, true);
  3. 加入以下檢驗 assert.equal(result.logs[0].args.name, zombieNames[0]);
const CryptoZombies = artifacts.require("CryptoZombies");
const zombieNames = ["Zombie 1", "Zombie 2"];
contract("CryptoZombies", (accounts) => {
    let [alice, bob] = accounts;
    it("should be able to create a new zombie", async () => {
        const contractInstance = await CryptoZombies.new();
        // start here
		const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
        assert.equal(result.receipt.status, true);
        assert.equal(result.logs[0].args.name, zombieNames[0]);
    })
})

針對 createZombie 功能檢核做測試

之前在 createZombie 功能有設定一個檢核 zombie 數量的限制

如下:

require(ownerZombieCount[msg.sender] == 0)

用來限制只能產生一個 zombie

然而,我們的測試會跑超過一次的 createZombie

為了能夠順利執行

我們需要使用 beforeEach 這個 Mocha 內定的 Hook

來確保每次的 contractInstance 都是全新的

如下:

beforeEach(async () => {
	const contractInstance = await CryptoZombies.new();
}

這樣一來, Truffle 就會確保每次測試值執行前都重新產生新的 contract instance

實作測試

  1. 宣告 let contractInstance;
  2. 加入 beforeEach 設定
  3. 把產生新的 contractInstance 邏輯放入 beforeEach
  4. 加入新的測試項目 it("should not allow two zombies")
const CryptoZombies = artifacts.require("CryptoZombies");
const zombieNames = ["Zombie 1", "Zombie 2"];
contract("CryptoZombies", (accounts) => {
    let [alice, bob] = accounts;

    // start here
    let contractInstance;
    beforeEach(async () => {
        contractInstance = await CryptoZombies.new();
    });
    it("should be able to create a new zombie", async () => {
        const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
        assert.equal(result.receipt.status, true);
        assert.equal(result.logs[0].args.name,zombieNames[0]);
    })

    //define the new it() function
    it("should not allow two zombies", async () => {

    })
})

關於 contract.new

每次做測試時, Truffle 會透過 contract.new 來產生一個新的 Contract

對測試來說這樣可以產生的狀態乾淨的 Contract 避免被狀態影響

然而這樣每次測試一次就出現一個新的測試 Contract

會造成鏈上過多無用的 Contract

為了避免這種狀框發生,會需要每個 Contract 自行定義一個 selfdestruct 來銷毀 Contract 當不需要使用時

selfdestruct 大致上流程如下:

  1. 會需要在 CryptoZombies Contract 加入以下功能
function kill() public onlyOwner {
	selfdestruct(owner());
}
  1. 下一步是在 afterEach() 加入 以下邏輯
afterEach(async () => {
	await contractInstance.kill();
})
  1. Truffle 會在每個測試結束之後呼叫 constractInstance.kill();

這樣就可以確保每個 Contract 在測試後被銷毀

實作檢測同一帳號不能使用 createRandomZombie 產生兩個 Zombie

測試邏輯如下

  1. alice 先用 zombieNames[0] 呼叫 createRandomZombie
  2. alice 再用 zombieNames[1] 呼叫 createRandomZombie
  3. 這時會預期 Contract 拋出 error
  4. 需要使用 try catch 語法來抓取這個 error

語法如下:

try {
    //try to create the second zombie
    await contractInstance.createRandomZombie(zombieNames[1], {from: alice});
    assert(true);
  }
  catch (err) {
    return;
  }
assert(false, "The contract did not throw.");

為了讓測試邏輯儘量簡化,所以會把上面的邏輯放到 helpers/util.js

然後在測試部份在引入

這時測試語法就可以簡化成以下

const utils = require("./helpers/utils");
await utils.shouldThrow(MyAwesomeContractInstance.myAwesomeFunction());

實作步驟

  1. 先讓 alice 用 zombieNames[0] 呼叫 createRandomZombie
  2. 再讓 alice 用 zombieNames[1] 呼叫 createRandomZombie,並且透過 shouldTrow 語法來檢測結果
const CryptoZombies = artifacts.require("CryptoZombies");
const utils = require("./helpers/utils");
const zombieNames = ["Zombie 1", "Zombie 2"];
contract("CryptoZombies", (accounts) => {
    let [alice, bob] = accounts;
    let contractInstance;
    beforeEach(async () => {
        contractInstance = await CryptoZombies.new();
    });
    it("should be able to create a new zombie", async () => {
        const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
        assert.equal(result.receipt.status, true);
        assert.equal(result.logs[0].args.name,zombieNames[0]);
    })
    it("should not allow two zombies", async () => {
        // start here
        await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
        await utils.shouldThrow(contractInstance.createRandomZombie(zombieNames[1], {from: alice}));
    })
})

檢測 Zombie Transfer

一共有兩種情境要檢測

第1種情境: Alice 使用 transferFrom 把 zombie 轉給 Bob

察看 transferFrom 如下:

function transferFrom(address _from, address _to, uint256 _tokenId) external payable;

所以需要把 alice 代入 _from , bob 代入 _to, 把 zombie id 帶入 _tokenId

然後檢測執行結果

第2種情境: Alice 先使用 approve 登計要轉讓 zombieId 給 Bob , Bob 或者 Alice 再呼叫 transferFrom 傳送 zombieId

察看使用的兩個 function:

function approve(address _approved, uint256 _tokenId) external payable;

function transferFrom(address _from, address _to, uint256 _tokenId) external payable;

Context function

為了讓測試具有結構性, Truffle 提供一個 context function 可以把測試分組,語法如下:

context("with the single-step transfer scenario", async () => {
    it("should transfer a zombie", async () => {
      // TODO: Test the single-step transfer scenario.
    })
})

context("with the two-step transfer scenario", async () => {
    it("should approve and then transfer a zombie when the approved address calls transferFrom", async () => {
      // TODO: Test the two-step scenario.  The approved address calls transferFrom
    })
    it("should approve and then transfer a zombie when the owner calls transferFrom", async () => {
        // TODO: Test the two-step scenario.  The owner calls transferFrom
     })
})

但如果這樣寫直接去跑 truffle test

會發現結果如下:

Contract: CryptoZombies
    ✓ should be able to create a new zombie (100ms)
    ✓ should not allow two zombies (251ms)
    with the single-step transfer scenario
      ✓ should transfer a zombie
    with the two-step transfer scenario
      ✓ should approve and then transfer a zombie when the owner calls transferFrom
      ✓ should approve and then transfer a zombie when the approved address calls transferFrom


  5 passing (2s)

還沒寫測試單卻都通過了!

問題在於沒做寫 assertion

但為了在還沒寫之前先跳過

可以先再 context 前加入一個 x,也就是 xcontext 代表要跳過的檢測

加入 xcontext 之後,執行 truffle test 結果會如下

Contract: CryptoZombies
    ✓ should be able to create a new zombie (199ms)
    ✓ should not allow two zombies (175ms)
    with the single-step transfer scenario
      - should transfer a zombie
    with the two-step transfer scenario
      - should approve and then transfer a zombie when the owner calls transferFrom
      - should approve and then transfer a zombie when the approved address calls transferFrom


  2 passing (827ms)
  3 pending
  • 出現在測項之前是因為使用了 xcontext 讓該測向跳過

實作內容

const CryptoZombies = artifacts.require("CryptoZombies");
const utils = require("./helpers/utils");
const zombieNames = ["Zombie 1", "Zombie 2"];
contract("CryptoZombies", (accounts) => {
    let [alice, bob] = accounts;
    let contractInstance;
    beforeEach(async () => {
        contractInstance = await CryptoZombies.new();
    });
    it("should be able to create a new zombie", async () => {
        const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
        assert.equal(result.receipt.status, true);
        assert.equal(result.logs[0].args.name,zombieNames[0]);
    })
    it("should not allow two zombies", async () => {
        await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
        await utils.shouldThrow(contractInstance.createRandomZombie(zombieNames[1], {from: alice}));
    })

    // start here
    xcontext("with the single-step transfer scenario", async () => {
        it("should transfer a zombie", async () => {
        // TODO: Test the single-step transfer scenario.
        })
    })

    xcontext("with the two-step transfer scenario", async () => {
        it("should approve and then transfer a zombie when the approved address calls transferFrom", async () => {
        // TODO: Test the two-step scenario.  The approved address calls transferFrom
        })
        it("should approve and then transfer a zombie when the owner calls transferFrom", async () => {
            // TODO: Test the two-step scenario.  The owner calls transferFrom
        })
    })
})

實作 single-step transfer 測試

  1. alice 先使用 zombieNames[0] 呼叫 createRandomZombie
  2. 宣告 const zombieId , 設定值為剛剛產生出來的 zombie 的 id
  3. alice 呼叫 transferFrom 轉移 zombieId 給 bob
  4. 宣告 const newOwner 設定值為 ownerOf zombieId
  5. 最後檢查 bob 是否擁有這個 zombieId
  6. 把 xcontext 改回 context
const CryptoZombies = artifacts.require("CryptoZombies");
const utils = require("./helpers/utils");
const zombieNames = ["Zombie 1", "Zombie 2"];
contract("CryptoZombies", (accounts) => {
    let [alice, bob] = accounts;
    let contractInstance;
    beforeEach(async () => {
        contractInstance = await CryptoZombies.new();
    });
    it("should be able to create a new zombie", async () => {
        const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
        assert.equal(result.receipt.status, true);
        assert.equal(result.logs[0].args.name,zombieNames[0]);
    })
    it("should not allow two zombies", async () => {
        await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
        await utils.shouldThrow(contractInstance.createRandomZombie(zombieNames[1], {from: alice}));
    })
    context("with the single-step transfer scenario", async () => {
        it("should transfer a zombie", async () => {
          // start here.
          const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
          const zombieId = result.logs[0].args.zombieId.toNumber();
          await contractInstance.transferFrom(alice, bob, zombieId, {from: alice});
          const newOwner = await contractInstance.ownerOf(zombieId);
          assert.equal(newOwner, bob); 
        })
    })
    xcontext("with the two-step transfer scenario", async () => {
        it("should approve and then transfer a zombie when the approved address calls transferFrom", async () => {
          // TODO: Test the two-step scenario.  The approved address calls transferFrom
        })
        it("should approve and then transfer a zombie when the owner calls transferFrom", async () => {
            // TODO: Test the two-step scenario.  The owner calls transferFrom
         })
    })
})

實作 two-step transfer 檢測

  1. alice 先使用 zombieNames[0] 呼叫 createRandomZombie
  2. alice 使用 zombieId, bob 呼叫 approve
  3. bob 呼叫 transferFrom 從 alice 接收 zombieId
  4. 檢查 bob 是否是 zombieId 的 owner
  5. 把 xcontext 更新為 context
const CryptoZombies = artifacts.require("CryptoZombies");
const utils = require("./helpers/utils");
const zombieNames = ["Zombie 1", "Zombie 2"];
contract("CryptoZombies", (accounts) => {
    let [alice, bob] = accounts;
    let contractInstance;
    beforeEach(async () => {
        contractInstance = await CryptoZombies.new();
    });
    it("should be able to create a new zombie", async () => {
        const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
        assert.equal(result.receipt.status, true);
        assert.equal(result.logs[0].args.name,zombieNames[0]);
    })
    it("should not allow two zombies", async () => {
        await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
        await utils.shouldThrow(contractInstance.createRandomZombie(zombieNames[1], {from: alice}));
    })
    context("with the single-step transfer scenario", async () => {
        it("should transfer a zombie", async () => {
            const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
            const zombieId = result.logs[0].args.zombieId.toNumber();
            await contractInstance.transferFrom(alice, bob, zombieId, {from: alice});
            const newOwner = await contractInstance.ownerOf(zombieId);
            assert.equal(newOwner, bob);
        })
    })
    context("with the two-step transfer scenario", async () => {
        it("should approve and then transfer a zombie when the approved address calls transferFrom", async () => {
            const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
            const zombieId = result.logs[0].args.zombieId.toNumber();
            // start here
            await contractInstance.approve(bob, zombieId, {from: alice});
            await contractInstance.transferFrom(alice, bob, zombieId, {from: bob});
            const newOwner = await contractInstance.ownerOf(zombieId);
            assert.equal(newOwner, bob);
        })
        xit("should approve and then transfer a zombie when the owner calls transferFrom", async () => {
            
         })
    })
})

實作 two-step transferFrom 第2種情境測試

  1. alice 先 approve
  2. alice 呼叫 transferFrom
const CryptoZombies = artifacts.require("CryptoZombies");
const utils = require("./helpers/utils");
const zombieNames = ["Zombie 1", "Zombie 2"];
contract("CryptoZombies", (accounts) => {
    let [alice, bob] = accounts;
    let contractInstance;
    beforeEach(async () => {
        contractInstance = await CryptoZombies.new();
    });
    it("should be able to create a new zombie", async () => {
        const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
        assert.equal(result.receipt.status, true);
        assert.equal(result.logs[0].args.name,zombieNames[0]);
    })
    it("should not allow two zombies", async () => {
        await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
        await utils.shouldThrow(contractInstance.createRandomZombie(zombieNames[1], {from: alice}));
    })
    context("with the single-step transfer scenario", async () => {
        it("should transfer a zombie", async () => {
            const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
            const zombieId = result.logs[0].args.zombieId.toNumber();
            await contractInstance.transferFrom(alice, bob, zombieId, {from: alice});
            const newOwner = await contractInstance.ownerOf(zombieId);
            assert.equal(newOwner, bob);
        })
    })
    context("with the two-step transfer scenario", async () => {
        it("should approve and then transfer a zombie when the approved address calls transferFrom", async () => {
            const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
            const zombieId = result.logs[0].args.zombieId.toNumber();
            await contractInstance.approve(bob, zombieId, {from: alice});
            await contractInstance.transferFrom(alice, bob, zombieId, {from: bob});
            const newOwner = await contractInstance.ownerOf(zombieId);
            assert.equal(newOwner,bob);
        })
        it("should approve and then transfer a zombie when the owner calls transferFrom", async () => {
            // TODO: start
            const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
            const zombieId = result.logs[0].args.zombieId.toNumber();
            await contractInstance.approve(bob, zombieId, {from: alice});
            await contractInstance.transferFrom(alice, bob, zombieId, {from: alice});
            const newOwner = await contractInstance.ownerOf(zombieId);
            assert.equal(newOwner,bob);
         })
    })
})

檢測 Zombie Attack

測試情境如下

  1. 先建立了二個 zombie:一個 owner 是 alice ,另一個是 bob
  2. 讓 bob zombie attack alice zombie
  3. 最後檢查 result.recepit.status 是不是 true

假設照著上面的邏輯去實作測試

然後執行 truffle test

會發現以下的結果

Contract: CryptoZombies
    ✓ should be able to create a new zombie (102ms)
    ✓ should not allow two zombies (321ms)
    ✓ should return the correct owner (333ms)
    1) zombies should be able to attack another zombie
    with the single-step transfer scenario
      ✓ should transfer a zombie (307ms)
    with the two-step transfer scenario
      ✓ should approve and then transfer a zombie when the approved address calls transferFrom (357ms)


  5 passing (7s)
  1 failing

  1) Contract: CryptoZombies
       zombies should be able to attack another zombie:
     Error: Returned error: VM Exception while processing transaction: revert

看起來是失敗

那原因是什麼呢?

首先察看 createZombie 邏輯

function createRandomZombie(string _name) public {
  require(ownerZombieCount[msg.sender] == 0);
  uint randDna = _generateRandomDna(_name);
  randDna = randDna - randDna % 100;
  _createZombie(_name, randDna);
}

然後再察看 _createZombie

function _createZombie(string _name, uint _dna) internal {
  uint id = zombies.push(Zombie(_name, _dna, 1, uint32(now + cooldownTime), 0, 0)) - 1;
  zombieToOwner[id] = msg.sender;
  ownerZombieCount[msg.sender] = ownerZombieCount[msg.sender].add(1);
  emit NewZombie(id, _name, _dna);
}

注意到在 _createZombie 邏輯裡使用了 cooldownTime 來減緩產生 zombie 的產生時間

而這個時間設定為一天

因此,直接測試會是失敗的

所以該怎麼處理這種狀況呢?

Time Travelling

在這種狀況下,為了能夠處理這類時間限制的問題

Ganache 節點提供了以下方法來處理時間測試

  • evm_increaseTime: 把下個區塊時間增加
  • evm_mine: 挖出一個新 block

以下示範如何使用

  1. 每次新的 block 被挖出來,挖礦節點會把 timestamp 附加到 block 上。
  2. 使用了 evm_increaseTime 把區塊時間增加了,但因為過去的區塊已寫入無法更改
  3. 所以只能透過 evm_mine 產生新的區塊把剛剛 evm_increaseTime 寫入做更新

更新語法如下:

await web3.currentProvider.sendAsync({
  jsonrpc: "2.0",
  method: "evm_increaseTime",
  params: [86400],  // there are 86400 seconds in a day
  id: new Date().getTime()
}, () => { });

web3.currentProvider.send({
    jsonrpc: '2.0',
    method: 'evm_mine',
    params: [],
    id: new Date().getTime()
});

以上的程式碼就可以讓 evm 時間條快 1 天

透過把這段邏輯封裝到 helpers/time.js

就可以透過 time.increaseTime(86400) 語法來模擬時間過了一天

但這樣可讀性還是不夠好

所以會透過 await time.increase(time.duration.days(1)) 這樣的語法來呼叫

實作

const CryptoZombies = artifacts.require("CryptoZombies");
const utils = require("./helpers/utils");
const time = require("./helpers/time");
const zombieNames = ["Zombie 1", "Zombie 2"];
contract("CryptoZombies", (accounts) => {
    let [alice, bob] = accounts;
    let contractInstance;
    beforeEach(async () => {
        contractInstance = await CryptoZombies.new();
    });
    it("should be able to create a new zombie", async () => {
        const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
        assert.equal(result.receipt.status, true);
        assert.equal(result.logs[0].args.name,zombieNames[0]);
    })
    it("should not allow two zombies", async () => {
        await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
        await utils.shouldThrow(contractInstance.createRandomZombie(zombieNames[1], {from: alice}));
    })
    context("with the single-step transfer scenario", async () => {
        it("should transfer a zombie", async () => {
            const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
            const zombieId = result.logs[0].args.zombieId.toNumber();
            await contractInstance.transferFrom(alice, bob, zombieId, {from: alice});
            const newOwner = await contractInstance.ownerOf(zombieId);
            assert.equal(newOwner, bob);
        })
    })
    context("with the two-step transfer scenario", async () => {
        it("should approve and then transfer a zombie when the approved address calls transferFrom", async () => {
            const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
            const zombieId = result.logs[0].args.zombieId.toNumber();
            await contractInstance.approve(bob, zombieId, {from: alice});
            await contractInstance.transferFrom(alice, bob, zombieId, {from: bob});
            const newOwner = await contractInstance.ownerOf(zombieId);
            assert.equal(newOwner,bob);
        })
        it("should approve and then transfer a zombie when the owner calls transferFrom", async () => {
            const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
            const zombieId = result.logs[0].args.zombieId.toNumber();
            await contractInstance.approve(bob, zombieId, {from: alice});
            await contractInstance.transferFrom(alice, bob, zombieId, {from: alice});
            const newOwner = await contractInstance.ownerOf(zombieId);
            assert.equal(newOwner,bob);
         })
    })
    it("zombies should be able to attack another zombie", async () => {
        let result;
        result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
        const firstZombieId = result.logs[0].args.zombieId.toNumber();
        result = await contractInstance.createRandomZombie(zombieNames[1], {from: bob});
        const secondZombieId = result.logs[0].args.zombieId.toNumber();
        //TODO: increase the time
        await time.increase(time.duration.days(1));
        await contractInstance.attack(firstZombieId, secondZombieId, {from: alice});
        assert.equal(result.receipt.status, true);
    })
})

Chai 語法

assert function 雖然能夠幫忙做到檢測

但其問題是可讀性不夠好

所以可以使用 Chai 這類輔助 library 來達到這件事情

Chai Assertion Library

Chai 是很強大的 assertion 函式庫。

主要有三類語法

  • expect: 讓使用者可以使用鏈狀判斷式
    如下
let lessonTitle = "Testing Smart Contracts with Truffle";
expect(lessonTitle).to.be.a("string");
  • should: 類似於 expect ,但 chain 會以 should 當作開始
    如下
let lessonTitle = "Testing Smart Contracts with Truffle";
lessonTitle.should.be.a("string");
  • assert: 提供語法支援 nodejs 與 browser 測試寫法
    如下
let lessonTitle = "Testing Smart Contracts with Truffle";
assert.typeOf(lessonTitle, "string");

以下將使用 expect 來做測試

引用語法如下

var expect = require('chai').expect;

to.equal()

let zombieName = 'My Awesome Zombie';
expect(zombieName).to.equal('My Awesome Zombie');

實作

  1. 引入 expect
  2. 把 assert.equal 取代為 expect
const CryptoZombies = artifacts.require("CryptoZombies");
const utils = require("./helpers/utils");
const time = require("./helpers/time");
//TODO: import expect into our project
var expect = require('chai').expect;
const zombieNames = ["Zombie 1", "Zombie 2"];
contract("CryptoZombies", (accounts) => {
    let [alice, bob] = accounts;
    let contractInstance;
    beforeEach(async () => {
        contractInstance = await CryptoZombies.new();
    });
    it("should be able to create a new zombie", async () => {
        const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
        //TODO: replace with expect
        expect(result.receipt.status).to.equal(true);
        expect(result.logs[0].args.name).to.equal(zombieNames[0]);
    })
    it("should not allow two zombies", async () => {
        await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
        await utils.shouldThrow(contractInstance.createRandomZombie(zombieNames[1], {from: alice}));
    })
    context("with the single-step transfer scenario", async () => {
        it("should transfer a zombie", async () => {
            const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
            const zombieId = result.logs[0].args.zombieId.toNumber();
            await contractInstance.transferFrom(alice, bob, zombieId, {from: alice});
            const newOwner = await contractInstance.ownerOf(zombieId);
            //TODO: replace with expect
            expect(newOwner).to.equal(bob);
        })
    })
    context("with the two-step transfer scenario", async () => {
        it("should approve and then transfer a zombie when the approved address calls transferFrom", async () => {
            const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
            const zombieId = result.logs[0].args.zombieId.toNumber();
            await contractInstance.approve(bob, zombieId, {from: alice});
            await contractInstance.transferFrom(alice, bob, zombieId, {from: bob});
            const newOwner = await contractInstance.ownerOf(zombieId);
            //TODO: replace with expect
            expect(newOwner).to.equal(bob);
        })
        it("should approve and then transfer a zombie when the owner calls transferFrom", async () => {
            const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
            const zombieId = result.logs[0].args.zombieId.toNumber();
            await contractInstance.approve(bob, zombieId, {from: alice});
            await contractInstance.transferFrom(alice, bob, zombieId, {from: alice});
            const newOwner = await contractInstance.ownerOf(zombieId);
            //TODO: replace with expect
            expect(newOwner).to.equal(bob);
         })
    })
    it("zombies should be able to attack another zombie", async () => {
        let result;
        result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
        const firstZombieId = result.logs[0].args.zombieId.toNumber();
        result = await contractInstance.createRandomZombie(zombieNames[1], {from: bob});
        const secondZombieId = result.logs[0].args.zombieId.toNumber();
        await time.increase(time.duration.days(1));
        await contractInstance.attack(firstZombieId, secondZombieId, {from: alice});
        //TODO: replace with expect
        expect(result.receipt.status).to.equal(true);
    })
})

透過 loom 來做測試

Loom 是一個測試鏈

可以使用比以太鏈快與使用不用 gas 的交易來測試 Contract

設定 Truffle 使用 Loom

必須修改連線網路的部份

 loom_testnet: {
      provider: function() {
        const privateKey = 'YOUR_PRIVATE_KEY';
        const chainId = 'extdev-plasma-us1';
        const writeUrl = 'wss://extdev-basechain-us1.dappchains.com/websocket';
        const readUrl = 'wss://extdev-basechain-us1.dappchains.com/queryws';
        return new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey);
      },
      network_id: 'extdev'
    }

accounts array

為了讓 Truffle 可以跟 Loom 節點溝通,需要修改預設 HDWalletProvider 為 Truffle Provider

然後必須要把產生幾個測試帳號都設定在 Provider 內

所以要修改

return new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey)

const loomTruffleProvider = new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey);
loomTruffleProvider.createExtraAccountsFromMnemonic(mnemonic, 10);
return loomTruffleProvider;

實作

  1. 修改回傳 LoomTruffleProvider
const HDWalletProvider = require("truffle-hdwallet-provider");
const LoomTruffleProvider = require('loom-truffle-provider');
const mnemonic = "YOUR MNEMONIC HERE";
module.exports = {
    // Object with configuration for each network
    networks: {
        //development
        development: {
            host: "127.0.0.1",
            port: 7545,
            network_id: "*",
            gas: 9500000
        },
        // Configuration for Ethereum Mainnet
        mainnet: {
            provider: function() {
                return new HDWalletProvider(mnemonic, "https://mainnet.infura.io/v3/<YOUR_INFURA_API_KEY>")
            },
            network_id: "1" // Match any network id
        },
        // Configuration for Rinkeby Metwork
        rinkeby: {
            provider: function() {
                // Setting the provider with the Infura Rinkeby address and Token
                return new HDWalletProvider(mnemonic, "https://rinkeby.infura.io/v3/<YOUR_INFURA_API_KEY>")
            },
            network_id: 4
        },
        // Configuration for Loom Testnet
        loom_testnet: {
            provider: function() {
                const privateKey = 'YOUR_PRIVATE_KEY';
                const chainId = 'extdev-plasma-us1';
                const writeUrl = 'wss://extdev-basechain-us1.dappchains.com/websocket';
                const readUrl = 'wss://extdev-basechain-us1.dappchains.com/queryws';
                // TODO: Replace the line below
                const loomTruffleProvider = new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey);
loomTruffleProvider.createExtraAccountsFromMnemonic(mnemonic, 10);
return loomTruffleProvider;
            },
            network_id: '9545242630824'
        }
    },
    compilers: {
        solc: {
            version: "0.4.25"
        }
    }
};


上一篇
從以太坊白皮書理解 web 3 概念 - Day15
下一篇
從以太坊白皮書理解 web 3 概念 - Day17
系列文
從以太坊白皮書理解 web 3 概念32
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言