メインコンテンツへスキップ
Change page

スマートコントラクトのテスト

最終編集者: @HiroyukiNaito(opens in a new tab), 2024年4月1日

イーサリアムなどのパブリックブロックチェーンは不変で、一度デプロイされたスマートコントラクトのコードを変更するのは困難です。 「バーチャルアップデート」を実行するためのスマートコントラクトアップグレードのパターンが存在するものの、実装は困難で、かつ社会的コンセンサスも必要です。 さらに、アップグレードは、発見された後にエラーを修正できるものにすぎません。攻撃者が先に脆弱性を発見した場合、スマートコントラクトは悪用される可能性があります。

これらの理由から、メインネットにデプロイする前にスマートコントラクトをテストすることは、 セキュリティの最小要件です。 テストでは、さまざまな技術を活用してコードの正確性を評価しますが、必要な機能や目的に合わせて選択しなければなりません。 それでも、さまざまなツールとアプローチを組み合わせたテストスイートを使えば、スマートコントラクトコード深刻度にかかわらず、セキュリティ欠陥を見つけることができます。

前提知識

このページでは、イーサリアムネットワークにデプロイする前に、スマートコントラクトをテストする方法について説明します。 スマートコントラクトの知識があることを前提としています。

スマートコントラクトのテストとは

スマートコントラクトのテストとは、スマートコントラクトのコードが意図したとおりに動作することを検証するプロセスです。 テストによって、スマートコントラクトの信頼性、使いやすさ、セキュリティ要件を満たしているかどうかを確認することができます。

テストにはさまざまなアプローチがありますが、一般的には、処理することが予想されるデータの小さなサンプルを用いてスマートコントラクトを実行する必要があります。 コントラクトがサンプルデータに対して正しい結果を生成する場合、コントラクトは適切に機能していると判断できます。 テストツールのほとんどは、コントラクトの実行が期待通りであるかどうかを検証するためのテストケース(opens in a new tab)を作成したり実行したりするためのリソースを提供しています。

スマートコントラクトのテストの重要性

スマートコントラクトでは、高価値の金融資産を管理することが多いため、小さなプログラミングの間違いがユーザーに大損失(opens in a new tab)を与える可能性があります。実際に、そのような事例は度々発生しています。 ただし、厳密なテストを行うことで、スマートコントラクトのコードの欠陥と問題点を早期に発見し、メインネットにリリースする前に修正することができます。

バグが発見された場合、コントラクトをアップグレードすることは可能ですが、アップグレードは複雑で、誤った処理をするとエラー(opens in a new tab)が発生する可能性があります。 また、アップグレードによって、コントラクトの不変性の原則が損なわれ、ユーザーにさらなる信頼が求められることになります。 一方で、統合的なテスト計画を立てることで、スマートコントラクトのセキュリティリスクを軽減し、デプロイ後に複雑なロジックのアップグレードを実行する必要性を減らすことができます。

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

イーサリアムのスマートコントラクトのテスト方法は、大きく分けて自動テスト手動テストの2つがあります。 自動テストと手動テストには、それぞれに長所と短所があります。両方を組み合わせることで、コントラクトを解析するための堅牢な計画を立てることができます。

自動テスト

自動テストでは、スマートコントラクトのコードを実行して、エラーがないか自動的にチェックするツールを使用します。 自動テストの利点は、スクリプト(opens in a new tab)を使用してコントラクトの機能を評価できることです。 スクリプト化されたテストは、人間の介入を最小限に抑え、繰り返し実行するようにスケジュールできるので、手動によるテストよりも効率的です。

自動テストは、テストが反復的で時間がかかる、手動で実行するのが難しい、人的ミスの影響を受けやすい、重要なコントラクト関数の評価を伴う、などの場合に特に役立ちます。 ただし、自動テストツールには、欠点もあります。例えば、特定のバグを検出できなかったり、多くの誤検知(opens in a new tab)をしてしまうことがあります。 そのため、スマートコントラクトのテストには、自動テストと手動テストを組み合わせることが望ましいと言えます。

手動テスト

手動テストは、人が直接操作して行うテストです。スマートコントラクトの正確性を解析する際には、テストスイートの各テストケースを順番に実行します。 これは、コントラクト上で複数の個別のテストを同時に実行し、失敗したテストと合格したすべてのテストを表示したレポートを取得できる自動テストとは異なります。

手動テストは、さまざまなテストシナリオを網羅するように作成されたテスト計画書に従って、個人が実行します。 また、手動テストの一環として、複数の個人やグループが一定期間にわたってスマートコントラクトを操作することもあります。 テスターは、コントラクトの実際の動作と期待される動作を比較して、違いがあればバグとしてフラグを立てます。

効果的な手動テストには、スキル、時間、資金、労力などの多くのリソースが必要になります。また、テストの実行中に人的ミスにより、特定のエラーを見逃すことがあります。 しかし、手動テストにもメリットがあります。例えば、人間のテスト担当者(監査人など)は、自動テストツールでは検出が難しいエッジケースを直感的に見つけることができます。

スマートコントラクトの自動テスト

単体テスト

単体テストでは、コントラクトの関数を個別に評価し、各コンポーネントが正しく動作するかを確認します。 優れた単体テストとは、シンプルで短時間で実行でき、テストが失敗した場合に、その原因を明確に示せるものです。

単体テストは、期待する値が返されるかどうかと、関数の実行後にコントラクトのストレージが適切に更新されるかどうかを確認するのに効果的です。 また、コントラクトのコードベースに変更を加えた後に単体テストを実行し、新しいロジックの追加によってエラーが発生しないことを確認します。 以下は、効果的な単体テストを実行するためのガイドラインです。

スマートコントラクト単体テストのガイドライン

1. コントラクトのビジネスロジックとワークフローを理解する

スマートコントラクトが提供する機能を理解し、ユーザーがどのように関数にアクセスして使用しているかを把握しておくと、単体テストを作成しやすくなります。 ハッピーパステスト(opens in a new tab)を実行する場合に特に有用です。これは、 コントラクト内の関数が有効なユーザーの入力に対して正しい出力を返すかどうかを判断します。 オークションコントラクト(opens in a new tab)の(要約された)例を用いて、このコンセプトを説明します。

1constructor(
2 uint biddingTime,
3 address payable beneficiaryAddress
4 ) {
5 beneficiary = beneficiaryAddress;
6 auctionEndTime = block.timestamp + biddingTime;
7 }
8
9function bid() external payable {
10
11 if (block.timestamp > auctionEndTime)
12 revert AuctionAlreadyEnded();
13
14 if (msg.value <= highestBid)
15 revert BidNotHighEnough(highestBid);
16
17 if (highestBid != 0) {
18 pendingReturns[highestBidder] += highestBid;
19 }
20 highestBidder = msg.sender;
21 highestBid = msg.value;
22 emit HighestBidIncreased(msg.sender, msg.value);
23 }
24
25 function withdraw() external returns (bool) {
26 uint amount = pendingReturns[msg.sender];
27 if (amount > 0) {
28 pendingReturns[msg.sender] = 0;
29
30 if (!payable(msg.sender).send(amount)) {
31 pendingReturns[msg.sender] = amount;
32 return false;
33 }
34 }
35 return true;
36 }
37
38function auctionEnd() external {
39 if (block.timestamp < auctionEndTime)
40 revert AuctionNotYetEnded();
41 if (ended)
42 revert AuctionEndAlreadyCalled();
43
44 ended = true;
45 emit AuctionEnded(highestBidder, highestBid);
46
47 beneficiary.transfer(highestBid);
48 }
49}
すべて表示

このオークションコントラクトは、入札期間中に入札を受け付けるシンプルなものです。 highestBidが増えた場合は、以前の最高入札者に入札金が支払われます。また、入札期間が終了すると、beneficiaryはコントラクトを呼び出して入札金を受け取ります。

このようなコントラクトの単体テストでは、コントラクトとやり取りする際にユーザーが呼び出す可能性のあるさまざまな関数をテストします。 例えば、オークションの進行中にユーザーが入札できるか(すなわち、bid()の呼び出しが成功する)かどうかを確認する単体テストや、ユーザーが現在のhighestBidよりも高い入札ができるかどうかを確認する単体テストなどがあります。

コントラクトの操作上のワークフローを理解しておくと、実行内容が要件を満たしているかどうかを確認する単体テストの作成にも役立ちます。 例えば、オークションコントラクトでは、オークションが終了したとき(つまり、auctionEndTimeblock.timestampよりも小さいとき)は、ユーザーが入札できないようになっています。 この場合、デベロッパーは、オークション終了時(つまり、auctionEndTime > block.timestampの場合)にbid()関数の呼び出しが成功するか失敗するかをチェックする単体テストを実行するとよいでしょう。

2. コントラクトの実行に関するすべての前提条件を評価する

コントラクトの実行に関する前提を文書化し、それらの前提の妥当性を検証する単体テストを作成することが重要です。 アサーションテストを行うことで、予期しない実行を防ぐことができます。また、スマートコントラクトのセキュリティモデルを破る可能性のある操作について検討する際にも役立ちます。 有用な方法としては、「ユーザーにとって満足のいくテスト」を超えて、間違った入力に対して関数が失敗するかどうかをチェックするネガティブテストを作成することです。

多くの単体テストフレームワークでは、アサーションの作成ができます。アサーションでは、コントラクトで可能・不可能なことを記述する単純なステートメントを作成します。テストを実行すると、それらのアサーションが維持されているかどうかが確認されます。 上記のオークションコントラクトを開発しているデベロッパーは、ネガティブテストを実行する前に、その挙動について次のようなアサーションを作成できます。

  • オークションが終了しているか、まだ開始されていない場合、ユーザーは入札することができないこと。

  • 入札額が許容しきい値を下回った場合、オークションコントラクトを取り消す(リバートする)こと。

  • 落札に失敗したユーザーの資金が確実に戻ること。

: 前提をテストする別の方法としては、特にrequireassertif…elseステートメントなど、コントラクト内の関数修飾子(opens in a new tab)をトリガーするテストを作成することです。

3. コードカバレッジを計測する

コードカバレッジ(opens in a new tab)は、テストで実行されたコード内の分岐、行、ステートメントの数を追跡する指標です。 テストには、適切なコードカバレッジが必要です。コードカバレッジが不十分だと、「偽陰性」を引き起こす可能性があります。つまり、コントラクトがすべてのテストに合格しても、依然としてコードに脆弱性が存在する可能性が残ってしまいます。 しかし、高いコードカバレッジを記録していれば、スマートコントラクト内のすべてのステートメントや関数が正確であることを十分に検証することができます。

4. 完成度の高いテストフレームワークを使用する

スマートコントラクトの単体テストを実行するツールの品質は、非常に重要です。 理想的なテストフレームワークは、定期的にメンテナンスされ、便利な機能(ログ機能やレポート機能など)を備えているものです。そして、多くのデベロッパーに広く使用され、よく精査されていることも重要です。

Solidityスマートコントラクト用の単体テストフレームワークは、さまざまな言語(主にJavaScript、Python、Rust)で提供されています。 単体テストの実行を始めるには、以下のさまざまなテストフレームワークのガイドを参照してください。

統合テスト

単体テストでは、コントラクトの関数を個別にデバッグしましたが、統合テストでは、スマートコントラクトのコンポーネント全体を評価します。 統合テストでは、スマートコントラクト間の呼び出しで発生する問題や、同じスマートコントラクト内の異なる関数間のやり取りで発生する問題を検出できます。 例えば、継承(opens in a new tab)や依存性注入などの機能が正しく動作するかどうかを確認するのに役立ちます。

統合テストは、コントラクトがモジュラー型アーキテクチャを採用していたり、実行中に他のオンチェーンコントラクトと接続する場合に有用です。 統合テストを実行する方法の1つは、(Forge(opens in a new tab)Hardhat(opens in a new tab)などのツールを使用して)ブロックチェーンの特定のブロックの高さで フォークすることです。そして、デプロイされたコントラクトと作成したコントラクトのやり取りをシミュレートします。

フォークされたブロックチェーンは、メインネットと同様の仕組みで動作し、アカウントに状態と残高が関連付けられています。 しかし、サンドボックス化されたローカル開発環境としてのみ機能します。例えば、トランザクションに実際のETHは必要なく、変更しても実際のイーサリアムプロトコルに影響することはありません。

プロパティベースのテスト

プロパティベースのテストは、スマートコントラクトが定義されたプロパティを満たしていることを確認するプロセスです。 プロパティは、コントラクトの行動に関する事実をアサーションします。この事実は、さまざまなシナリオにおいて真であることが期待されるものです。スマートコントラクトプロパティの例としては、「コントラクト内の算術演算は、オーバーフローもアンダーフローもしない」などがあります。

プロパティベースのテストを実行する方法には、静的分析動的分析の2つの一般的な手法があります。どちらの手法でも、 プログラムのコード(この場合は、スマートコントラクト)が、事前に定義されたプロパティを満たしていることを検証できます。 プロパティベースのテストツールには、予期されるコントラクトプロパティに対する事前定義されたルールが備えてあり、コードがそれらのルールに違反しているかチェックするものや、スマートコントラクトのカスタムプロパティを作成できるものがあります。

静的解析

静的アナライザーは、スマートコントラクトのソースコードを受け取って解析し、コントラクトがプロパティを満たしているかどうかを判断します。 動的解析とは異なり、静的解析では、コントラクトを実行して正確性の解析を行うことはありません。 静的解析は、スマートコントラクトが実行中にたどる可能性のあるすべてのパスを推論します。つまり、ソースコードの構造を調べて、コントラクトの操作がランタイムで何を意味するかを決定します。

コントラクトで静的解析を実行する一般的な手法として、リンティング(opens in a new tab)静的テスト(opens in a new tab)があります。 どちらの手法も、コンパイラによって出力された抽象構文木(opens in a new tab)制御フローグラフ(opens in a new tab)など、コントラクト実行における低レベル表現の解析が必要です。

静的解析は、安全でない構造の使用や構文エラー、コントラクトコード内のコーディング規約違反などの安全性の問題を検出するには有効です。 しかし、より深い脆弱性の検出が不得意であることが知られており、過剰な誤検出が生じる可能性があります。

動的解析

動的解析では、シンボリックな入力(例: シンボリック実行(opens in a new tab))または具体的な入力(例: ファジング(opens in a new tab))をスマートコントラクトの関数に生成して、実行トレースが特定のプロパティに違反していないかどうかを確認します。 この方式によるプロパティベースのテストでは、単体テストとは異なり、複数のシナリオのテストケースをカバーし、プログラムがテストケースを生成します。

ファジング(opens in a new tab)は、動的解析手法の例の1つでスマートコントラクトの任意のプロパティを検証します。 ファザー(fuzzer)は、定義されたランダムまたは不正なバリエーションの入力値で、ターゲットコントラクト内の関数を呼び出します。 スマートコントラクトがエラー状態 (例: アサーションが失敗した状態など) になると、問題に対してフラグが立てられ、実行によって脆弱なパスに向かう入力がレポートに作成されます。

ファジングは、スマートコントラクトの入力検証メカニズムを評価するのに効果的です。なぜなら、予期しない入力を適切に処理しないと、意図しない動作が発生し、悪影響が生じる可能性があるからです。 この方式によるプロパティベースのテストは、以下のような理由から理想的です。

  1. さまざまなシナリオをカバーするテストケースを書くことは難しい。 プロパティテストでは、振る舞いとその振る舞いをテストするためのデータ範囲を定義するだけです。プログラムは、定義されたプロパティに基づいてテストケースを自動的に生成します。

  2. テストスイートがプログラム内のすべての実行パスを十分にカバーしていないことがある。100%のカバレッジであっても、エッジケースを見逃す可能性があります。

  3. 単体テストでは、コントラクトがサンプルデータに対して正しく実行されることを証明できるが、サンプル外の入力に対して正しく実行されるかどうかは未確認のままである。プロパティテストでは、ターゲットコントラクトを複数のバリエーションで実行します。 指定された入力値を使用して、アサーションの失敗を引き起こす実行トレースを見つけます。 そのため、プロパティテストでは、広範なクラスの入力データに対してコントラクトが正しく実行されることを、より確実に保証することができます。

スマートコントラクトでプロパティベースのテストを実行する際のガイドライン

プロパティベースのテストの実行では通常、プロパティの定義(例: 整数オーバーフロー(opens in a new tab)がないこと)、またはスマートコントラクト検証を行う必要のあるプロパティのコレクションを定義することから始めます。 プロパティテストを作成するときに、プログラムがトランザクションの入力データを生成するために、その値の範囲の定義が必要になることもあります。

プロパティテストツールは、適切に構成することで、ランダムに生成された入力値を使ってスマートコントラクトの関数を実行することができます。 アサーション違反が発生した場合、評価対象のプロパティに違反する具体的な入力データがレポートに含まれます。 プロパティベースのテストを実行するには、以下のさまざまなツールのガイドを参照してください。

スマートコントラクトの手動テスト

スマートコントラクトの手動テストは、通常、自動テストを行った後の開発サイクルの後半で行われます。 この手動テストでは、スマートコントラクトを完全に統合された1つの製品として評価し、技術要件で指定されたとおりの性能を発揮するかどうかを確認します。

ローカルブロックチェーンでのコントラクトのテスト

ローカルの開発環境で実行される自動テストは、有用なデバッグ情報を提供しますが、実際の環境でスマートコントラクトがどのように動作するかを確認したい場合もあります。 しかし、実際のイーサリアムチェーンにデプロイするとガス代が発生します。また、スマートコントラクトにバグがある場合、ユーザーが実際にお金を失う可能性があることは言うまでもありません。

メインネットでテストする代わりに、ローカルのブロックチェーン(開発ネットワークとも呼ばれます)でコントラクトをテストすることをお勧めします。 ローカルブロックチェーンは、イーサリアムブロックチェーンのコピーで、自分のコンピュータのローカル環境で実行できるものです。これにより、イーサリアムの実行レイヤーの動作をシミュレートすることができます。 そのため、トランザクションをプログラムしてコントラクトとやり取りする際に、多額のコストが発生することはありません。

ローカルのブロックチェーン上でコントラクトを実行するのは、手動で統合テストを行うのに効果的な方法です。 スマートコントラクトは、柔軟に構成できるようになっています。これにより、既存のプロトコルと統合できるようになりましたが、複雑なオンチェーンでの相互作用が正しい結果を生み出すかどうかについては、依然として確認する必要があります。

開発用ネットワークの詳細

テストネットでのスマートコントラクトのテスト

テストネットワークすなわちテストネットは、イーサリアムメインネットとまったく同じ仕様で動作するネットワークです。ただし、テストネットで使用されるイーサ(ETH)は、現実世界で価値がありません。 コントラクトをテストネットにデプロイすると、資金を失うリスクはありません。また、Dappのフロントエンドなどを介して、誰でもコントラクトとやり取りできるようになります。

この方式の手動テストでは、ユーザーの視点からアプリケーションのエンドツーエンドのフローを評価することができます。 テストネットでは、ベータテスターが試験運用を行い、コントラクトのビジネスロジックや全体的な機能に関する問題を報告することもできます。

テストネットの方がイーサリアム仮想マシンの動作に近いため、ローカルブロックチェーンでテストした後にテストネットにデプロイすることが理想です。 そのため、多くのイーサリアムを使うプロジェクトでは、テストネットにDappをデプロイし、現実世界の条件下でスマートコントラクトの操作を評価するのが一般的です。

イーサリアムテストネットの詳細

テストと形式検証の比較

テストは、あるデータの入力に対して、コントラクトが期待通りの結果を返すことを確認するのに役立ちますが、テストで使用されていない入力に対して、同じことを確実に証明できるわけではありません。 したがって、スマートコントラクトのテストでは、「機能的な正しさ」を保証できません。つまり、入力値のすべてのセットに対して、プログラムが要求通りに動作することを保証することはできません。

形式検証は、プログラムの形式モデルが形式仕様と一致するかどうかを確認することでソフトウェアの正確さを評価するアプローチです。 プログラムを抽象的かつ数学的に表現したものが形式モデルで、プログラムのプロパティ(つまり、プログラムの実行に関する論理的アサーション)を定義したものが形式仕様です。

プロパティは、数学用語で記述されるため、システムの形式(数学)モデルが仕様を満たしていることを、論理的な推論規則を使用して検証することができます。 したがって、形式検証ツールは、システムの正確さを「数学的に証明」できると言われています。

テストとは異なり、形式検証は、サンプルデータを使わずにスマートコントラクトがどのような実行においても、形式仕様を満たしていること(バグがないこと)を検証できます。 これにより、数十の単体テストの実行時間が短縮され、背後にある脆弱性をより効率的に発見できるようになります。 ただし、形式検証手法では、実装の難易度や有用性が多岐に渡ります。

スマートコントラクトの形式検証の詳細

テストと監査およびバグ報奨金の比較

上記のように、厳密なテストをしても、コントラクトにバグがないとは言い切れません。 形式検証によるアプローチは、正確性をより強力に保証できますが、現時点では使用が難しく、かなりのコストがかかります。

第三者によるコードレビューを受ければ、コントラクトの脆弱性を発見できる可能性が高くなります。 第三者にコントラクトを分析してもらうには、スマートコントラクトの監査(opens in a new tab)およびバグ報奨金(opens in a new tab)のいずれかの方法があります。

監査は、スマートコントラクトのセキュリティ上の欠陥や不健全な開発プラクティスを探し出す経験豊かな監査人が行います。 監査では、コードベース全体の手動レビューだけでなく、テストや形式検証も行われるのが一般的です。

一方で、バグ報奨金プログラムでは通常、スマートコントラクトの脆弱性を発見してデベロッパーへ公開する(ホワイトハットハッカー(opens in a new tab)と呼ばれる)個人に対して、金銭的な報酬が支払われます。 バグ報奨金は、スマートコントラクトの不具合の発見を他の人に依頼するもので、監査に似ています。

主な違いとしては、バグ報奨金プログラムは、より広範なデベロッパーやハッカーコミュニティを対象としているため、ユニークなスキルや経験を持つ幅広いクラスの倫理的なハッカーや独立したセキュリティ専門家を引きつけることができます。 これは、限られた専門知識を持つチームに依存するスマートコントラクト監査では得られない利点と言えるでしょう。

テストツールとライブラリ

単体テストツール

  • solidity-coverage(opens in a new tab) - Solidityで書かれたスマートコントラクトのコードカバレッジツール

  • Waffle(opens in a new tab) - 高度なスマートコントラクトの開発とテストのためのフレームワーク(ethers.jsベース)

  • Remixテスト(opens in a new tab) - Solidityスマートコントラクトのテストツール。 Remix IDEの「Solidity Unit Testing」プラグインで動作します。このプラグインは、コントラクトのテストケースの作成と実行に使用します。

  • OpenZeppelinテストヘルパー(opens in a new tab) - イーサリアムスマートコントラクトのテストに使用できるアサーションライブラリ。 コントラクトが期待通りに動作していることを確認してください。

  • Brownieユニットテストフレームワーク(opens in a new tab) - 最小限のコードで小さなテストを作成可能。また、大規模なプロジェクトにも対応するスケーラビリティも持ち合わせており、機能豊富なテストフレームワークであるPytestを利用しています。

  • Foundryテスト(opens in a new tab) - Foundry社は、シンプルな単体テスト、ガス最適化チェック、コントラクトファジングを実行でき、高速で柔軟なイーサリアムテストフレームワークであるForgeを提供しています。

  • Hardhatテスト(opens in a new tab) - ethers.js、Mocha、Chaiをベースとしたスマートコントラクトテストにおけるフレームワーク

  • ApeWorx(opens in a new tab) - イーサリアム仮想マシン用のスマートコントラクトでPythonベースの開発およびテストのためのフレームワーク

プロパティベースのテストツール

静的解析ツール

  • Slither(opens in a new tab) - PythonベースのSolidity静的解析フレームワークで、脆弱性の発見、コード理解の強化、スマートコントラクトのカスタム解析の記述で使用

  • Ethlint(opens in a new tab) - スマートコントラクトのプログラミング言語であるSolidityのスタイルとセキュリティのベストプラクティスを適用するためのリンター

動的解析ツール

  • Echidna(opens in a new tab) - プロパティベースのテストによりスマートコントラクトの脆弱性を検出する高速なコントラクトファザー

  • Diligence Fuzzing(opens in a new tab) - スマートコントラクトのコードのプロパティ違反を検出するのに便利な自動ファジングツール

  • Manticore(opens in a new tab) - EVMバイトコード解析のための動的シンボリック実行フレームワーク

  • Mythril(opens in a new tab) - Taint解析、Concolic解析、制御フローチェックを用いてコントラクトの脆弱性を検出するEVMバイトコード評価ツール

  • Diligence Scribble(opens in a new tab) - Scribbleは、仕様記述言語およびランタイム検証ツールでスマートコントラクトのプロパティにアノテーションを付けることができます。これにより、Diligence FuzzingやMythXなどのツールを使用してコントラクトを自動的にテストできます。

参考文献

この記事は役に立ちましたか?