用 async await 寫出簡潔的 Truffle 測試
第一次大量寫 Solidity 的測試是在 LogoVote 活動中,測試投票的合約。最近在公司也須為新的契約撰寫測試,學到很多新花招。本文將以重構 LogoVote 索取獎金的測試為範例,展示這些讓開發測試更有效率的技巧。
本文假設對智能合約有大致了解,了解 JavaScript 非同步機制,被回呼地獄整過,可能也被 Promise 地獄雷過。
索取獎金的測試
在索取獎金的測試中,大概有如下步驟:
- 部署 LogoVote 主合約
- 讓兩位作者:作者一號與二號註冊 Logo
- 一位投票者取得投票代幣並投票給作者二號
- 調區塊高度,快轉至投票結束
- LogoVote 主合約管理者執行
claimWinner
讓契約計算贏家 - 斷言贏家應為作者二號
- 兩位作者分別執行
claimReward
索取報酬 - 斷言兩位作者索取的報酬與規格相符
一些小細節是:
- 測試用主合約並非真的 LogoVote 。而是繼承 LogoVote 的一個 Mock,上面實作了調區塊高度的函數,來測試不同時間點的行為
- 我中間貪心做了其他斷言,例如投票者應取得正確數量的代幣 (42 行)
- 註冊 Logo 會產生一個 Logo 契約,
claimReward
要從 Logo 契約調用 - 報酬分配方式是贏家先拿 50% 契約總金額,剩下 50% 再分給包含贏家的所有人。測試範例中,只有一個投票者投 2.2 Ether ,贏家作者二拿了 50% 與剩下 50% 的一半,共 75% ,作者一拿 25%
Promise Hell 版本
若理解了測試主要的邏輯,接下來請用愉悅的心情觀看這份程式碼,那時撰寫時主要參考 Joye 早先寫的測試與 Truffle 官方的寫法。
以上程式碼讀完可能稍有不適,原因主要在於:
- 很長
- 一串 Promise 火車,以 then 串連每個車廂。因為有許多非同步執行的函式。
- 車廂內做的事情很難看出明確意義。
- 變數宣告到賦值隔了一段距離,喜歡變數一定義就不能改(immutable)的人會覺得很醜。因為變數必須跨車廂使用,要在火車開始前宣告。但又在中間車廂才會實現,必須晚點賦值。
用 Async Await 脫離地獄
後來經過咏宸介紹看到 Open Zeppelin 的 zeppelin solidity 專案。這個專案很建議去看看,裡面有很多精美契約與測試的範例。
從 zeppelin solidity 學到的第一件事,就是用 Async Await 去讓程式碼變乾淨。
使用 Async Await 要注意兩件事。第一件是小事,必須把 nodejs 升級到 7 以上的版本,才有原生支援 Async Await。第二件事,Async Await 是建立在 Promise 的概念上。必須理解 Promise 的行為,才能善用 async 和 await。不精確大原則是:
- 如果函式回傳是 Promise ,在前面加個 await 就能把該 Promise 等完
- 在函式前加個 async,函式回傳的東西會裝在 Promise 裡
改造後的程式如下:
同時,裡面一些冗長重複的函式萃取出來,增加可讀性。
整體而言,修改後的程式碼
- 短些了
- 變數可以馬上賦值
- 程式沒有非同步代碼所帶來閱讀的不適感,每一行都是獨立的一件事
- 由上,更容易把程式碼拆分成 Arrange Act Assert (AAA 模式) 的三個區塊。
還有什麼能做的
拆開太長的程式碼
程式碼長最主要的原因還是因為測試的故事太長了。契約部署、註冊 Logo 等,可拆解成其他函式去測試。
describe('behavior after contract deployed')
describe('behavior after end block reached')
在文末參考資料 Arrange Act Assert 一文中,有句話說得好,試翻如下:
不要在 Arrange 程式碼區塊中斷言。如果你會需要這麼做,這是一個 Arrange 太複雜的味道。你必須把 Arrange 內的東西抽出 fixture 或 setup 函式,並幫他們另外撰寫測試
思考觸發函式的角色 有助理解使用者行為與發掘漏洞
在 contract
一開始就定義好所有的角色。這是我想養成的習慣,明確定義的角色名稱,能夠在閱讀程式碼時了解使用者的行為。
contract('LogoVote', function (accounts) {
let contract_owner_account = accounts[0]
let author1_account = accounts[1]
let author1_owner_account = accounts[2]
let author2_account = accounts[3]
let author2_owner_account = accounts[4]
let voter1_account = accounts[5]
...
同時,契約裡非 constant 的函式都代表有人得花手續費去執行,會變動鏈上的資料。在 Truffle 的 web3 裡,呼叫契約函式裡面塞 {from: somebody_account}
,這筆交易就能夠從 somebody_account
這個帳戶發出。手續費會從這個帳戶出,對交易發起者的特定效果也會發生在這個帳戶上。
因此很重要的是,必須理解這些會變動到鏈上資料的函式由誰執行會有什麼效果。對的人執行會不會有預期的效果,錯的人執行會不會有非預期的效果。幸運的話可以人工發現一些契約的潛在問題。
不選擇交易發出的帳號,在 Truffle 中仍能執行函式。但指明執行的角色,可協助開發者從人的角度去思考。下面是先前範例程式碼中,忘記指名角色的錯誤示範。
// bad: 讀者不知道誰執行了這函式
await logoVote.claimWinner()
// good: 讀者知道原來是契約擁有者執行的
await logoVote.claimWinner({from: contract_owner_account})
結語
撰寫智能合約就好像是一般網站開發中,把後端與金流系統都開源。在各種高手開發出能加強契約安全的外星科技前,自動化測試是現有最好的工具來保障程式碼品質。至少在契約流程複雜時,能夠感受到自動化測試保護了正常流程的順利。(不正常的流程就 …)
雖然可以測試智能合約的 Truffle 框架已日益成熟,良好撰寫風格仍仰賴學習門檻不低的 JavaScript。我也還在踩雷之路之上,若有更好的做法,歡迎留言回應交流。
> 更新:增加了環境必須使用 nodejs 7 的說明
參考資料
關於 LogoVote 活動的介紹
LogoVote 的原始碼
Truffle 官方文件的測試寫法
Open Zeppelin 的 zeppelin solidity 專案
理解 Promise 再來用 async await
Arrange Act Assert 以及撰寫測試的語句、原則