䟋ずしおDAOを䜿甚したEthereumスマヌトコントラクトのテスト

Ethereumプラットフォヌムでスマヌトコントラクトを䜜成する堎合、開発者はメ゜ッドがコントラクトの状態をどのように倉曎するか、どのむベントを発行するか、い぀、誰に資金を転送する必芁があるか、い぀䟋倖をスロヌするかを定矩する特定の䜜業ロゞックを䜜成したす。 スマヌトコントラクト甚のデバッグツヌルはただあたり開発されおいないため、テストはしばしば必芁な開発ツヌルになりたす。 各倉曎埌に契玄を実行するこずは、非垞に長いプロセスになる可胜性がありたす。 たた、゚ラヌが発生した堎合、ネットワヌクにデプロむされた契玄のコヌドを倉曎するこずはすでに䞍可胜です。契玄を砎棄しお新しい契玄を䜜成するこずしかできないため、テスト、特に支払い方法をできるだけ培底的に実行する必芁がありたす。 この蚘事では、 Solidityのスマヌトコントラクトを䜜成およびデバッグするずきに開発者が盎面するいく぀かのテスト手法を玹介したす。



分散型自埋組織DAO



2016幎の倏、THE DAOずの物語はセンセヌショナルであり、攻撃者はそこから倚額の資金を盗みたした 。 DAOは組織ずしお䜍眮付けられるスマヌトな契玄であり、そのすべおのプロセスはブロックチェヌン環境で動䜜するコヌドによっお蚘述されたすが、法人ではなく、すべおの投資家によっお集合的に管理されたす。 3月に、DAO開発者はテストの重芁性を匷調し、 PythonずJavascriptを混合したフレヌムワヌクを䜿甚したテストでスマヌトコントラクトをカバヌしたしたが、残念ながらテストは埌の脆匱性を閉じたせんでした。



たずえば、 DAOスマヌトコントラクトコヌドは倧きすぎるため、テストオブゞェクトずしお、 http //ethereum.orgのブロックチェヌンに民䞻䞻矩を構築する方法の蚘事で説明されおいるDAOの原則を実装する議䌚スマヌトコントラクトを取り䞊げたす。 将来的には、スマヌトコントラクトの開発の基本原則に粟通しおいるこずが前提ずなりたす。



スマヌトコントラクトのテスト方法



䞀般的な原則は、他のコヌドのテストに䌌おいたす-䞀連の参照メ゜ッド呌び出しが事前定矩された環境で䜜成され、結果ステヌトメントが曞き蟌たれたす。 テストには、BDD-Behavior Driven Development Practicesを䜿甚するず䟿利です。テストず䞀緒に、ドキュメントず䜿甚䟋を䜜成できたす。



テストツヌル



倚くのむヌサリアムスマヌトコントラクトテストフレヌムワヌクずラむブラリが開発されたした。



トリュフ



Truffle v.2では、MochaフレヌムワヌクずChaiラむブラリを䜿甚しお、JavaScriptでテストが開発されおいたす。 バヌゞョン3では、 Solidityのテストを䜜成する機胜が远加されおいたす 。



ただら



DAppleでは、特別に開発された基本的なスマヌトコントラクトの方法を䜿甚しお、Solidityにテストが実装されたす。



Embarkjs



EmbarkJSでは、アプロヌチはTruffleに䌌おおり、テストはJavascriptで蚘述され、Mochaフレヌムワヌクが䜿甚されたす。



Solidityのテストの開発はこの蚀語の機胜によっお非垞に制限されおいるため、Javascriptを䜿甚したす。すべおの䟋ではTruffle Frameworkを䜿甚したす。 たた、 truffle-contractやtruffle-artifactorなどのTruffle Frameworkコンポヌネントを䜿甚しお、スマヌトコントラクトず察話するためのカスタム゜リュヌションを䜜成できたす。



テストクラむアント



ブロックチェヌンシステム、特にむヌサリアムは非垞に高速に動䜜しないため、テストには「テスト」ブロックチェヌンクラむアントを䜿甚したす。たずえば、 むヌサリアムクラむアントのJSON RPC APIをほが完党に゚ミュレヌトするTestRPCです。 TestRPCは、暙準のメ゜ッドに加えお、テスト時にevm_increaseTime 、 evm_mineなどの䜿甚に䟿利ないく぀かの远加メ゜ッドも実装したす。



別の方法ずしお、暙準クラむアントの1぀を䜿甚する方法がありたす。たずえば、パリティはdevモヌドで動䜜し 、トランザクションは即座に確認されたす。 さらなる䟋では、TestRPCが䜿甚されたす。



環境蚭定



テストクラむアント



npmを介したむンストヌル



npm install -g ethereumjs-testrpc
      
      





TestRPCは別のタヌミナルで実行する必芁がありたす。 テストを開始するたびに、テストクラむアントは10個の新しいアカりントを生成したす。各アカりントには既に資金が配眮されおいたす。



トリュフフレヌムワヌク



npmを介したむンストヌル



 npm install -g truffle
      
      





プロゞェクト構造を䜜成するには、truffle initコマンドを実行する必芁がありたす



 $ mkdir solidity-test-example $ cd solidity-test-example/ $ truffle init
      
      





コントラクトはコントラクト/ディレクトリに配眮する必芁がありたす。コントラクトをコンパむルするずき、Truffle Frameworkは各コントラクトが個別のファむルに配眮されるこずを期埅し、コントラクトの名前はファむルの名前ず等しくなりたす。 テストはtest /ディレクトリに配眮されたす。 truffle initコマンドが実行されるず 、Metacoin et al。テストコントラクトも䜜成されたす。



さらなる䟋では、プロゞェクトhttps://github.com/vitiko/solidity-test-exampleが䜿甚されたす。これには、 議䌚のスマヌト契玄コヌドずそのテストが含たれおいたす。 テストはTruffle v.2環境で実行されたす。最近リリヌスされたv.3バヌゞョンでは、Truffleによっお生成されたコヌドの接続ず、状態倉曎メ゜ッドを呌び出した埌に返されるトランザクションデヌタ圢匏にわずかな違いがありたす。



Truffleフレヌムワヌクに基づいたテスト開発



テスト組織



テストではJavaScriptオブゞェクトを䜿甚したす。これは、コントラクトの操䜜、オブゞェクトの操䜜ずEthereumクラむアントメ゜ッドのJSON RPC呌び出し間のマッピングの抜象化です。 これらのオブゞェクトは、゜ヌスコヌド* .solファむルのコンパむル時に自動的に䜜成されたす。 すべおのメ゜ッドの呌び出しは非同期であり、Promiseを返したす。これにより、トランザクション確認の远跡を心配する必芁がなくなり、すべおがTruffleコンポヌネントの「内郚」で実装されたす。



Truffle Frameworkサむトの䟋では、.thenチェヌンを䜿甚した蚘述スタむルを䜿甚しおいたす。 倧芏暡なシナリオを説明する堎合、テストコヌドは非垞に膚倧です。 async / awaitを䜿甚したテストコヌドははるかに簡朔で読みやすいため、このスタむルのテストを䜿甚したす。 たた、開発者のサむトの䟋では、スマヌトコントラクトのむンスタンスが䜿甚されおおり、そのデプロむは移行で芏定されおいたす 。 移行ずテストむンスタンスの䜜成を混圚させないために、テストで明瀺的に䜜成する方が䟿利です。このため、各テスト関数を呌び出す前に、コントラクトの新しいむンスタンスを䜜成するコヌドを䜿甚できたす。 以䞋の䟋は、 Congressオブゞェクトのむンスタンスが䜜成されるbeforeEach関数を瀺しおいたす。



コングレススマヌトコントラクトデザむナヌ
 /* First time setup */ function Congress( uint minimumQuorumForProposals, uint minutesForDebate, int marginOfVotesForMajority, address congressLeader ) payable { changeVotingRules(minimumQuorumForProposals, minutesForDebate, marginOfVotesForMajority); if (congressLeader != 0) owner = congressLeader; // It's necessary to add an empty first member addMember(0, ''); // and let's add the founder, to save a step later addMember(owner, 'founder'); }
      
      





 const congressInitialParams = { minimumQuorumForProposals: 3, minutesForDebate: 5, marginOfVotesForMajority: 1, congressLeader: accounts[0] }; let congress; beforeEach(async function() { congress = await Congress.new(...Object.values(congressInitialParams)); });
      
      





スマヌトコントラクトのステヌタスをテストする



たず、スマヌトコントラクトの状態を倉曎するaddMemberメ゜ッドをテストしおみたしょう。このメ゜ッドは、 メンバヌ構造の配列にDAOメンバヌに関する情報を曞き蟌む必芁がありたす。



スマヌトコントラクトのaddMember関数コヌド
 /*make member*/ function addMember(address targetMember, string memberName) onlyOwner { uint id; if (memberId[targetMember] == 0) { memberId[targetMember] = members.length; id = members.length++; members[id] = Member({member: targetMember, memberSince: now, name: memberName}); } else { id = memberId[targetMember]; Member m = members[id]; } MembershipChanged(targetMember, true); }
      
      





テストでは、テストアカりントで配列を䜿甚しお、参加者を契玄に远加したす。 次に、members関数 members構造䜓の配列のゲッタヌが入力されたデヌタを返すこずを確認したす。 addMemberメ゜ッドを呌び出すたびに、トランザクションが䜜成され、ブロックチェヌンの状態が倉化するこずに泚意しおください。 情報は分散レゞストリに蚘録されたす。



 it("should allow owner to add members", async function() { //  3  for (let i = 1; i <= 3; i++) { let addResult = await congress.addMember(accounts[i], 'Name for account ' + i); //    members      2. // .. members[0] - empty, members[1] - ,   (. ) let memberInfoFromContract = await congress.members(i + 1); //  members(pos)       Member //  [0] -   , [1] -    assert.equal(memberInfoFromContract[0], accounts[i]); assert.equal(memberInfoFromContract[1], 'Name for account ' + i); } });
      
      





むベントテスト



むヌサリアムのむベントには、かなり普遍的なアプリケヌションがあり、䜿甚できたす。





次の䟋では、 議䌚の契玄に提案を远加するnewProposalメ゜ッドを呌び出すずきに、 Proposal Addedむベントのレコヌドが䜜成されるこずを確認したす



機胜コヌドnewProposalスマヌトコントラクト
 /* Function to create a new proposal */ function newProposal( address beneficiary, uint etherAmount, string JobDescription, bytes transactionBytecode ) onlyMembers returns (uint proposalID) { proposalID = proposals.length++; Proposal p = proposals[proposalID]; p.recipient = beneficiary; p.amount = etherAmount; p.description = JobDescription; p.proposalHash = sha3(beneficiary, etherAmount, transactionBytecode); p.votingDeadline = now + debatingPeriodInMinutes * 1 minutes; p.executed = false; p.proposalPassed = false; p.numberOfVotes = 0; ProposalAdded(proposalID, beneficiary, etherAmount, JobDescription); numProposals = proposalID+1; return proposalID; }
      
      





これを行うには、テストで最初にDAOメンバヌを䜜成し、圌に代わっお提案を䜜成したす。 次に、 ProposalAddedむベントのサブスクラむバヌを䜜成し、 newProposalメ゜ッドの呌び出し埌にむベントが発生し、その属性が送信されたデヌタに察応するこずを確認したす。



 it("should fire event 'ProposalAdded' when member add proposal", async function() { let proposedAddedEventListener = congress.ProposalAdded(); const proposalParams = { beneficiary : accounts[9], etherAmount: 100, JobDescription : 'Some job description', transactionBytecode : web3.sha3('some content') }; await congress.addMember(accounts[5], 'Name for account 5'); await congress.newProposal(...Object.values (proposalParams), { from: accounts[5] }); let proposalAddedLog = await new Promise( (resolve, reject) => proposedAddedEventListener.get( (error, log) => error ? reject(error) : resolve(log) )); assert.equal(proposalAddedLog.length, 1, 'should be 1 event'); let eventArgs = proposalAddedLog[0].args; assert.equal(eventArgs.proposalID , 0); assert.equal(eventArgs.recipient , proposalParams.beneficiary); assert.equal(eventArgs.amount , proposalParams.etherAmount); assert.equal(eventArgs.description , proposalParams.JobDescription); }); });
      
      





゚ラヌテストずメッセヌゞ送信者の怜蚌



コントラクトメ゜ッドの操䜜を䞭断する暙準的なメ゜ッドは、 throwステヌトメントを䜿甚しお䜜成できる䟋倖です。 メ゜ッドぞのアクセスを制限する必芁がある堎合など、䟋倖が必芁になる堎合がありたす。 これを行うために、メ゜ッドを呌び出したアカりントのアドレスをチェックする修食子が実装され、条件を満たさない堎合、䟋倖がスロヌされたす。 たずえば、コントラクトの所有者がaddMemberメ゜ッドを呌び出した堎合に䟋倖がスロヌされるこずを確認するテストを䜜成したしょう。 以䞋のコヌドでは、 議䌚の契玄がアカりント[0]に代わっお䜜成され、次にaddMemberメ゜ッドが別のアカりントに代わっお呌び出されたす。



 it("should disallow no owner to add members", async function() { let addError; try { //  ,  accounts[0] != accounts[9] await congress.addMember(accounts[1], 'Name for account 1', { from: accounts[9] }); } catch (error) { addError = error; } assert.notEqual(addError, undefined, 'Error must be thrown'); //      ,       //  "invalid JUMP" assert.isAbove(addError.message.search('invalid JUMP'), -1, 'invalid JUMP error must be returned'); });
      
      





スマヌトコントラクトの珟圚の時刻を䜿甚しお、スマヌトコントラクトのバランスの倉化をテストする



おそらく、DAOの原則を実装する議䌚のスマヌトコントラクトの最も重芁な機胜は、 executeProposal機胜です。この機胜は、提案が必芁な投祚数を受け取ったこずを確認し、提案の議論がスマヌトコントラクトの䜜成時に蚭定された最䜎必芁時間以䞊続いた埌、受益者に送金したす議論䞭の提案。



機胜コヌドexecuteProposalスマヌトコントラクト
 function executeProposal(uint proposalNumber, bytes transactionBytecode) { Proposal p = proposals[proposalNumber]; /* Check if the proposal can be executed: - Has the voting deadline arrived? - Has it been already executed or is it being executed? - Does the transaction code match the proposal? - Has a minimum quorum? */ if (now < p.votingDeadline || p.executed || p.proposalHash != sha3(p.recipient, p.amount, transactionBytecode) || p.numberOfVotes < minimumQuorum) throw; /* execute result */ /* If difference between support and opposition is larger than margin */ if (p.currentResult > majorityMargin) { // Avoid recursive calling p.executed = true; if (!p.recipient.call.value(p.amount * 1 ether)(transactionBytecode)) { throw; } p.proposalPassed = true; } else { p.proposalPassed = false; } // Fire Events ProposalTallied(proposalNumber, p.currentResult, p.numberOfVotes, p.proposalPassed); }
      
      





経過時間をシミュレヌトするために、 testrpcに 実装されおいるevm_increaseTimeメ゜ッドを䜿甚したす。これを䜿甚しお、クラむアントブロックチェヌンの内郚時間を倉曎できたす。



 it("should pay for executed proposal", async function() { const proposalParams = { beneficiary: accounts[9], etherAmount: 1, JobDescription: 'Some job description', transactionBytecode: web3.sha3('some content') }; //    accounts[9]   executeProposal let curAccount9Balance = web3.eth.getBalance(accounts[9]).toNumber(); //  ,         //     accounts[9] await congress.newProposal(...Object.values(proposalParams), { from: accounts[0] //accounts[0]  , ..      }); //  DAO,      //     3    //   3  DAO     0    for (let i of[3, 4, 5]) { await congress.addMember(accounts[i], 'Name for account ' + i); await congress.vote(0, true, 'Some justification text from account ' + i, { from: accounts[i] }); } //    let curProposalState = await congress.proposals(0); //   ,       //  .  //... //    testrpc  10 ,    //       minutesForDebate (5) await new Promise((resolve, reject) => web3.currentProvider.sendAsync({ jsonrpc: "2.0", method: "evm_increaseTime", params: [10 * 600], id: new Date().getTime() }, (error, result) => error ? reject(error) : resolve(result.result)) ); //    -  3  “”, //      await congress.executeProposal(0, proposalParams.transactionBytecode); // ,     accounts[9] //   ,     let newAccount9Balance = web3.eth.getBalance(accounts[9]).toNumber(); assert.equal(web3.fromWei(newAccount9Balance - curAccount9Balance, 'ether'), proposalParams.etherAmount, 'balance of acccounts[9] must increase to proposalParams.etherAmount'); let newProposalState = await congress.proposals(0); assert.isOk(newProposalState[PROPOSAL_PASSED_FIELD]); });
      
      





ヘルパヌ関数



テストの蚘述プロセスでは、䟋倖のチェック、むベントログの受信、テストクラむアントの時刻の倉曎など、テスト䞭に頻繁に䜿甚される操䜜に䟿利な機胜が圹立ちたした。 関数はパッケヌゞhttps://www.npmjs.com/package/solidity-test-utilでホストされ、コヌドはgithubでホストされたす。 以䞋は、 testUtil.assertThrow関数を䜿甚しお䟋倖をチェックする䟋です。



 it("should disallow no owner to add members", async function() { await testUtil.assertThrow(() => congress.addMember(accounts[1], 'Name for account 1', { from: accounts[9] })); });
      
      





solidity-test-util関数を䜿甚した他の䟋に぀いおは、 こちらをご芧ください 。



おわりに



テストの開発の結果、スマヌトコントラクトの䜜業の正確性の自動怜蚌ず、䞀般に、他のプログラムコヌドのテストに぀いお蚀えるすべおの仕様ず䜿甚䟋の䞡方が埗られたす。



All Articles