---
# System prepended metadata

title: 從以太坊白皮書理解 web 3 概念 - Day16
tags: [' Web 3', it 鐵人賽 30 天, ' ethereum']

---

---
tags: it 鐵人賽 30 天, Web 3, ethereum
---
# 從以太坊白皮書理解 web 3 概念 - Day16

## Learn Solidity - Day 8 - Testing Smart Contracts with Truffle

前面講述關於 Smart Contract 的撰寫

今天將會透過 [Lession 11 - Testing Smart Contracts with Truffle](https://cryptozombies.io/en/lesson/11) 來學關於如何測試 Smart Contract

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

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

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

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

還有透過 Chai 撰寫測試

## 環境設定

假設以前幾章節的 Project ZombieContract

其在 Truffle 的架構下

專案檔案架構應該如下

```javascript=
├── 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 資料下

```shell=
touch test/CryptoZombies.js
```

## 建立 artifacts

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

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

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

語法如下：

```javascript=
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 會真正實作測試本身。
	 
範例如下：

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

### 撰寫測試

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

```javascript=
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


語法如下
```javascript=
let [alice, bob] = accounts;
```

### 回顧建立 Zombie Contract 的部份

```solidity=
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 前面，為了比較好撰寫非同步邏輯

```javascript=
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 名稱來呼叫

設定的部份會如下:

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

執行的部份則會如下

```javascript=
contractInstance.createRandomZombie(zombieNames[0]);
```

### 撰寫測試設定部份

1. 宣告 const contractInstance = await CryptoZombies.new();

```javascript=
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

如下:

```javascript=
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]);

```javascript=
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 數量的限制

如下:

```solidity=
require(ownerZombieCount[msg.sender] == 0)
```
用來限制只能產生一個 zombie

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

為了能夠順利執行

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

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

如下：

```javascript=
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") 

```javascript=
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 加入以下功能

```solidity=
function kill() public onlyOwner {
	selfdestruct(owner());
}
```

2. 下一步是在 afterEach() 加入 以下邏輯

```javascript=
afterEach(async () => {
	await contractInstance.kill();
})
```

3. Truffle 會在每個測試結束之後呼叫 constractInstance.kill();

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

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

測試邏輯如下

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

語法如下:
```javascript=
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

然後在測試部份在引入

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

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

### 實作步驟

1. 先讓 alice 用 zombieNames[0] 呼叫 createRandomZombie
2. 再讓 alice 用 zombieNames[1] 呼叫 createRandomZombie，並且透過 shouldTrow 語法來檢測結果

```javascript=
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 如下：
```solidity=
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:

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

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

### Context function

 為了讓測試具有結構性， Truffle 提供一個 context function 可以把測試分組，語法如下:
 
```javascript=
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

會發現結果如下:

```javascript
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 結果會如下

```javascript=
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 讓該測向跳過

### 實作內容

```javascript=
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

```javascript=
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

```javascript=
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 

```javascript=
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

會發現以下的結果
```javascript=
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 邏輯
```solidity=
function createRandomZombie(string _name) public {
  require(ownerZombieCount[msg.sender] == 0);
  uint randDna = _generateRandomDna(_name);
  randDna = randDna - randDna % 100;
  _createZombie(_name, randDna);
}

```

然後再察看 _createZombie

```solidity=
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 寫入做更新

更新語法如下：

```javascript=
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)) 這樣的語法來呼叫

### 實作

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

### 以下將使用 expect 來做測試

引用語法如下
```javascript=
var expect = require('chai').expect;
```

#### to.equal() 

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

### 實作

1. 引入 expect
2. 把 assert.equal 取代為 expect

```javascript=
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 

必須修改連線網路的部份

```javascript=
 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 內

所以要修改
```javascript=
return new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey)
```
為
```javascript=
const loomTruffleProvider = new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey);
loomTruffleProvider.createExtraAccountsFromMnemonic(mnemonic, 10);
return loomTruffleProvider;
```

### 實作

1. 修改回傳 LoomTruffleProvider

```javascript=
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"
        }
    }
};

```