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