BrownieでSmart Contractのテストを書く
概要
Ethereum上にデプロイされたSmart Contractはデプロイ後にアップデートすることができない。そのため、デプロイ前に十分な動作検証を行う必要がある。動作検証を行うにあたり、pythonでテストを書くことができるpopulusが一般的なフレームワークであったが、開発が停止している。現在pythonでテストを書くことが可能なフレームワークとしてBrownieがある。本稿ではBrownieを用いてSmart Contractのテストを実施する手順を記す。
環境
Brownieのインストール
Brownieの依存ツールとしては、python3、pip、ganache-cliが挙げられている。 ganache-cliはSolidityのフレームワークtruffleのチームが開発している開発しているEthereum RPCクライアントだ。今回はCLIツールを用いる。npmでインストールするので、以下のコマンドでインストールする。
npm install -g ganache-cli
Brownie自体はpipでインストールすることが可能だ。
pip install eth-brownie
インストールされたことを確認する。
chike[~]brownie Using solc version v0.5.9 Brownie v1.0.0b7 - Python development framework for Ethereum Usage: brownie <command> [<args>...] [options <args>]
また、コンパイルにsoldityコンパイラであるsolcが必要なため、brewでインストールする。
brew install solidity
Browineプロジェクトの作成とディレクトリ構成
brownie init projectname
のコマンドでプロジェクトを新規作成できる。
chike[~]brownie init test Using solc version v0.5.9 Brownie v1.0.0b7 - Python development framework for Ethereum Brownie environment has been initiated at /Users/chike/test
プロジェクトディレクトリ以下に新規に作成されるディレクトリは以下の構成となる。
chike[~]tree test test ├── brownie-config.json // brownieプロジェクトの設定ファイル ├── contracts // solidity/viperのソースファイル ├── reports // JSON形式のBrownie GUIでのログファイル ├── scripts // デプロイや実行に関わるスクリプト └── tests // テストコード 4 directories, 1 file
そのほかにもbrownieでコンパイルを行うとbuild
ディレクトリが作成され、その中にバイトコードのコントラクトやテストデータが入る。
また、brownieにはテンプレートがいくつか用意されており、以下のコマンドでテンプレートを使った新規作成が行える。
brownie bake token // tokenを扱うテンプレートで新規作成
コンパイルとテストの実行
brownieを使ってコンパイルする際のsolcのバージョンをbrownie-config.json
の中で指定している。指定されたバージョンのsolcがないとそのバージョンのsolcをインストールしようとする。1
今回は、solcのバージョンは0.5.9を利用しているので、brownie-config.json
の当該部分を書き換える。以下は書き換え部分の抜粋。
"solc":{ "optimize": true, "runs": 200, "version": "0.5.9" },
プロジェクトディレクトリ直下でbrownie complie
のコマンドでcontracts
ディレクトリにあるソースコードをコンパイルする。今回はbakeしたtokenをコンパイルしてみる。
chike[token]brownie compile Using solc version v0.5.9 Brownie v1.0.0b7 - Python development framework for Ethereum Using solc version v0.5.9 Compiling contracts... Optimizer: Enabled Runs: 200 - Token.sol... - SafeMath.sol... Brownie project has been compiled at /Users/chike/Documents/workspace/brownietest/token/build/contracts
次に、brownie test
でtests
ディレクトリにあるテストが実行される。ひとまずtokenのそのまま実行してみる。
chike[token]brownie test Using solc version v0.5.9 Brownie v1.0.0b7 - Python development framework for Ethereum Using solc version v0.5.9 Running 6 tests across 2 modules. Running /Users/chike/Documents/workspace/brownietest/token/tests/transfer.py - 1 test (1/2) ✓ 0 - setup (0.1654s) ✓ 1 - Transfer tokens (0.1567s) Completed /Users/chike/Documents/workspace/brownietest/token/tests/transfer.py (0.4863s) Running /Users/chike/Documents/workspace/brownietest/token/tests/approve_transferFrom.py - 5 tests (2/2) ✓ 0 - setup (0.1373s) ⊝ 1 - balance (skipped) ✓ 2 - Set approval (0.2117s) ✓ 3 - Transfer tokens with transferFrom (0.2492s) ✓ 4 - transerFrom should revert (0.1418s) ‼ 5 - This test is expected to fail (AttributeError) Completed /Users/chike/Documents/workspace/brownietest/token/tests/approve_transferFrom.py (1.5656s) Total runtime: 2.2292s SUCCESS: All tests passed.
また、特定のテストファイルのテストのみを実行したい場合はbrownie test testfile
で実行できる。
テストスクリプトの記述法
brownieはそもそもコントラクトのフレームワークなので、CLIツールとしてインタラクティブシェルがあり、デプロイやトランザクションの発行などの操作が可能になっている。具体的な各操作は公式ドキュメントのCLIの操作を見るのが一番手っ取り早い。
テストスクリプトは全てtests
ディレクトリ以下に設置する。設置されたファイルはbrownieにテスト用スクリプトとして見なされるので、何か使い回しの処理をしたいときはscripts
ディレクトリに設置し、importするようにする。
まずはbrownieのメソッドを利用可能にするためにfrom brownie import *
する。その後、テストを開始する際はsetup
関数を最初に実行する仕組みになっているので、コントラクトの初期セットアップなどはsetup
関数の中に記述する。その後は各関数がそれぞれ上から順に1ユニットテストとして扱われる。
また、関数名直後にコメントを書き込むことで、テスト実行時にテストユニット名として表示される。下記サンプルと先ほどのテスト結果の項目を見比べれば理解できるだろうか。
以下はbrownieでbakeしたtokenのtestサンプルスクリプトである。ここでは、scripts
ディレクトリ内にTokenコントラクトをデプロイするスクリプトが記述されており、それを用いてコントラクトデプロイを行なっている。
#!/usr/bin/python3 from brownie import * import scripts.token def setup(): scripts.token.main() global token token = Token[0] def balance(skip=True): check.equal( token.balanceOf(accounts[0], "1000 ether"), "Accounts 0 balance is wrong" ) def approve(): '''Set approval''' token.approve(accounts[1], "10 ether", {'from': accounts[0]}) check.equal( token.allowance(accounts[0], accounts[1]), "10 ether", "Allowance is wrong" ) check.equal( token.allowance(accounts[0], accounts[2]), 0, "Allowance is wrong" ) token.approve(accounts[1], "6 ether", {'from': accounts[0]}) check.equal( token.allowance(accounts[0], accounts[1]), "6 ether", "Allowance is wrong" ) def transfer(): '''Transfer tokens with transferFrom''' token.approve(accounts[1], "6 ether", {'from': accounts[0]}) token.transferFrom( accounts[0], accounts[2], "5 ether", {'from': accounts[1]} ) check.equal( token.balanceOf(accounts[2]), "5 ether", "Accounts 2 balance is wrong" ) check.equal( token.balanceOf(accounts[1]), 0, "Accounts 1 balance is wrong" ) check.equal( token.balanceOf(accounts[0]), "995 ether", "Accounts 0 balance is wrong" ) check.equal( token.allowance(accounts[0], accounts[1]), "1 ether", "Allowance is wrong" ) def revert(): '''transerFrom should revert''' check.reverts( token.transferFrom, (accounts[0], accounts[3], "10 ether", {'from': accounts[1]}) ) check.reverts( token.transferFrom, (accounts[0], accounts[2], "1 ether", {'from': accounts[0]}) ) def unfinished(pending=True): '''This test is expected to fail''' token.secretFunction(accounts[1], "10 ether")
skipとpending
各関数で引数skip
を設定し、skip
がTrueの時はそのテストはスキップされる。また、引数pending
を設定し、Trueにした場合、そのテストは実行されるが、失敗してもエラーの詳細は表示されない。
アカウント操作
テスト実行のたびに、それぞれ100Ether持ったアカウントが10個初期値として生成される。各accountオブジェクトは変数accounts
内にリストとして保持されている。
accountsオブジェクトのメソッドとしては、保有Ether量を出力するbalance()
、コントラクトのデプロイを行うdeploy()
、送金を行うtransfer()
などが用意されている。
コントラクトのデプロイ・操作
contract
以下に記述された各コントラクトファイルは、テスト実行時にコンパイルされ、正常にコンパイルされればコントラクト名でコントラクトのオブジェクトを参照できる。このコントラクトオブジェクトをアカウントオブジェクトのdeploy()
メソッドを用いることで、ローカルテスト環境にデプロイできる。deploy()
メソッドでは第1引数にコントラクトオブジェクト、第2引数以降にコントラクトコンストラクタの引数を指定する。
t = accounts[0].deploy(Token, "Test Token", "TST", 18, "1000 ether")
また、ここでは用いていないが、最終引数には、トランザクションの付与情報をdict形式で記述することができる。ここで指定できる付与情報は、web3のeth.send.Transactionのパラメータを指定することができる。例えばコントラクトデプロイ時に一定金額デポジットを要求するような場合、ここでvalue
を指定することで送金しながらのデプロイトランザクションの発行をテストできる。
デプロイされたコントラクトは、ここで指定したコントラクトオブジェクトにリスト的に格納されている。そのため、テスト中に最初にデプロイしたコントラクトはToken[0]
の形でそのオブジェクトにアクセスできる。
コントラクトの各関数は、デプロイされたコントラクトオブジェクトのメソッドとしてアクセスすることができる。
まとめ
自分は普段ずっとpythonを主な使用言語にしているので、pythonでこうしたテストがかけるのは非常にありがたい。また、アップデート困難なブロックチェーン上のアプリケーションをテストするのは非常に重要である。一方で、開発者の想定外/テスト外のバグは完全に潰すことはほぼ不可能なので、そもそも根本的にブロックチェーンを改善する必要があるが、それはまた別の話だろう。
-
自分の環境では別バージョンsolcをmakeする際にエラーが発生したのだが、本稿の主眼ではないので追求しなかった。↩