前面講述關於 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
每次當編譯一個 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 做互動
在背後, Truffle 有使用 Mocha 這個測試的框架為了簡化測試。
以下會是接下會去實作的部份:
範例如下:
contract("MyAwesomeContract", (accounts) => {
it("should be able to receive Ethers", () => {
})
})
const CryptoZombies = artifacts.require("CryptoZombies");
contract("CryptoZombies", (accounts) => {
it("should be able to create a new zombie", () => {
})
})
在發佈到 Ethereum 鏈上前,可以先在自己架設的 Ganache 節點測試一下。
每次開啟 Gancache 節點時,Gancache 會自動建立 10 個測試帳號,每個初始化 100 Ether。
因為 Truffle 與 Gancache 整合的很好,所以可以直接透過 accounts 這個陣列直接取得帳號的 address
語法如下
let [alice, bob] = accounts;
function createRandomZombie(string _name) public {
require(ownerZombieCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
randDna = randDna - randDna % 100;
_createZombie(_name, randDna);
}
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
})
})
測試邏輯可以分成3個步驟
舉個例子:
假設要測試 MyAwesomeContract 的建立新 createRandomZombie 功能
因為還需要傳入 zombie 名稱來呼叫
設定的部份會如下:
const contractInstance = await MyAwesomeContract.new();
//
const zombieNames = ["Zombie #1", "Zombie #2"];
執行的部份則會如下
contractInstance.createRandomZombie(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 () => {
// start here
const constractInstance = await CryptoZombies.new();
})
})
面臨的第一個問題是? 如何設定 ZombieOwner
透過 abi ,可以透過帶入 from 參數設定 owner
如下:
const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
第二個問題是 result 的格式是什麼?
在 Truffle 從 artifacts.require 初始化 abi 後,
Truffle 會自動透過 Smart Contract 產生 log 。
舉例來說:
result.logs[0].args.name 就可以取的執行後的 zombie 名稱。
此外基上 , result 還有其他重要的結構如下
驗證的部份可以透過 assertion function 如 equal 或是 deepEqual 來做。但這邊只會查單純的值,所以只會用 assert.equal()
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 功能有設定一個檢核 zombie 數量的限制
如下:
require(ownerZombieCount[msg.sender] == 0)
用來限制只能產生一個 zombie
然而,我們的測試會跑超過一次的 createZombie
為了能夠順利執行
我們需要使用 beforeEach 這個 Mocha 內定的 Hook
來確保每次的 contractInstance 都是全新的
如下:
beforeEach(async () => {
const contractInstance = await CryptoZombies.new();
}
這樣一來, Truffle 就會確保每次測試值執行前都重新產生新的 contract instance
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 () => {
})
})
每次做測試時, Truffle 會透過 contract.new 來產生一個新的 Contract
對測試來說這樣可以產生的狀態乾淨的 Contract 避免被狀態影響
然而這樣每次測試一次就出現一個新的測試 Contract
會造成鏈上過多無用的 Contract
為了避免這種狀框發生,會需要每個 Contract 自行定義一個 selfdestruct 來銷毀 Contract 當不需要使用時
selfdestruct 大致上流程如下:
function kill() public onlyOwner {
selfdestruct(owner());
}
afterEach(async () => {
await contractInstance.kill();
})
這樣就可以確保每個 Contract 在測試後被銷毀
測試邏輯如下
語法如下:
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());
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}));
})
})
一共有兩種情境要檢測
第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;
為了讓測試具有結構性, 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
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
})
})
})
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
})
})
})
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 () => {
})
})
})
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);
})
})
})
測試情境如下
假設照著上面的邏輯去實作測試
然後執行 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 的產生時間
而這個時間設定為一天
因此,直接測試會是失敗的
所以該怎麼處理這種狀況呢?
在這種狀況下,為了能夠處理這類時間限制的問題
Ganache 節點提供了以下方法來處理時間測試
以下示範如何使用
更新語法如下:
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);
})
})
assert function 雖然能夠幫忙做到檢測
但其問題是可讀性不夠好
所以可以使用 Chai 這類輔助 library 來達到這件事情
Chai 是很強大的 assertion 函式庫。
主要有三類語法
let lessonTitle = "Testing Smart Contracts with Truffle";
expect(lessonTitle).to.be.a("string");
let lessonTitle = "Testing Smart Contracts with Truffle";
lessonTitle.should.be.a("string");
let lessonTitle = "Testing Smart Contracts with Truffle";
assert.typeOf(lessonTitle, "string");
引用語法如下
var expect = require('chai').expect;
let zombieName = 'My Awesome Zombie';
expect(zombieName).to.equal('My Awesome Zombie');
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 是一個測試鏈
可以使用比以太鏈快與使用不用 gas 的交易來測試 Contract
必須修改連線網路的部份
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'
}
為了讓 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;
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"
}
}
};