🐷
Solidity基礎学習28日目(Hardhatにおけるテスト)
HardhatにおけるSolidityテストとTypeScriptテストの比較
日付: 2025年9月28日
学習内容: Hardhatを用いたテストケースの書き方、SolidityテストとTypeScriptテストの比較
1. Hardhatのテスト機能の概要
1.1 Hardhat 3のテスト環境
Hardhat 3では、2つの異なるテスト方式をサポートしています:
-
Solidityテスト: Foundry/Forge形式のテスト(
.t.solファイル) -
TypeScriptテスト: Node.js/Viem/Ethersを使用したテスト(
.tsファイル)
両方のテスト形式を同時に使用でき、npx hardhat testコマンドで両方が実行されます。
1.2 テストファイルの構成
hardhat/
├── contracts/
│ ├── Counter.sol # 本体のコントラクト
│ └── Counter.t.sol # Solidityで書かれたテスト
└── test/
└── Counter.ts # TypeScriptで書かれたテスト
2. Solidityテスト(Counter.t.sol)の詳細
2.1 基本構造
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;
import {Counter} from "./Counter.sol";
import {Test} from "forge-std/Test.sol";
contract CounterTest is Test {
Counter counter;
function setUp() public {
counter = new Counter();
}
function test_InitialValue() public view {
require(counter.x() == 0, "Initial value should be 0");
}
function testFuzz_Inc(uint8 x) public {
for (uint8 i = 0; i < x; i++) {
counter.inc();
}
require(counter.x() == x, "Value after calling inc x times should be x");
}
function test_IncByZero() public {
vm.expectRevert();
counter.incBy(0);
}
}
2.2 Solidityテストの特徴
メリット:
- オンチェーン実行: 実際のEVM環境で実行される
- ガスコスト最適化: ガス使用量を直接測定可能
-
ファズテスト:
testFuzz_プレフィックスで自動的にランダムテスト -
チートコード:
vm.expectRevert()など強力な検証機能 - コンパクト: テストロジックがシンプルで読みやすい
デメリット:
- 外部依存の制限: 外部APIやデータベースへのアクセスが困難
- デバッグの難しさ: TypeScriptほどデバッグツールが充実していない
- 学習曲線: Solidityの知識が必要
2.3 Solidityテストの命名規則
| プレフィックス | 用途 | 例 |
|---|---|---|
test |
通常のテストケース | test_InitialValue() |
testFuzz_ |
ファジングテスト | testFuzz_Inc(uint8 x) |
testFail |
失敗を期待するテスト | testFail_Unauthorized() |
invariant |
不変条件のテスト | invariant_BalanceSum() |
3. TypeScriptテスト(Counter.ts)の詳細
3.1 基本構造
import { expect } from "chai";
import { describe, it } from "node:test";
import { network } from "hardhat";
describe("Counter", async function () {
const { viem } = await network.connect();
const publicClient = await viem.getPublicClient();
it("Should emit the Increment event when calling the inc() function", async function () {
const counter = await viem.deployContract("Counter");
await viem.assertions.emitWithArgs(
counter.write.inc(),
counter,
"Increment",
[1n],
);
});
it("The sum of the Increment events should match the current value", async function () {
const counter = await viem.deployContract("Counter");
const deploymentBlockNumber = await publicClient.getBlockNumber();
// run a series of increments
for (let i = 1n; i <= 10n; i++) {
await counter.write.incBy([i]);
}
const events = await publicClient.getContractEvents({
address: counter.address,
abi: counter.abi,
eventName: "Increment",
fromBlock: deploymentBlockNumber,
strict: true,
});
// check that the aggregated events match the current value
let total = 0n;
for (const event of events) {
total += event.args.by;
}
expect(total).to.equal(await counter.read.x());
});
});
3.2 TypeScriptテストの特徴
メリット:
- 豊富なツール: デバッガ、テストフレームワーク、アサーションライブラリ
- 外部統合: API、データベース、他のサービスとの連携が容易
- 詳細なテスト: イベント、トランザクション、状態変化の詳細な検証
- 開発者フレンドリー: JavaScript/TypeScriptの知識で書ける
デメリット:
- 実行速度: Solidityテストより遅い場合がある
- セットアップの複雑さ: 依存関係の管理が必要
- 間接的な実行: RPC経由でのテスト実行
3.3 使用可能なライブラリ
| ライブラリ | 用途 | インストール |
|---|---|---|
| Viem | Web3インタラクション | @nomicfoundation/hardhat-toolbox-viem |
| Chai | アサーション | npm install --save-dev chai @types/chai |
| Mocha | テストランナー(オプション) | npm install --save-dev mocha @types/mocha |
| Ethers | Web3インタラクション(代替) | npm install --save-dev ethers |
4. テスト手法の比較表
4.1 機能比較
| 機能 | Solidityテスト | TypeScriptテスト |
|---|---|---|
| 実行環境 | EVM内部 | Node.js + RPC |
| テスト速度 | 高速 | 中速 |
| ガス測定 | ネイティブサポート | 間接的 |
| ファズテスト | 組み込み | ライブラリ必要 |
| イベント検証 | 基本的 | 詳細 |
| 外部データ | 制限あり | 自由 |
| デバッグ | 基本的 | 高度 |
| 学習難易度 | 中~高 | 低~中 |
4.2 ユースケース比較
| ユースケース | 推奨テスト方式 | 理由 |
|---|---|---|
| 基本的な機能テスト | Solidity | シンプルで高速 |
| ガス最適化 | Solidity | 直接的な測定が可能 |
| ファズテスト | Solidity | ネイティブサポート |
| 統合テスト | TypeScript | 外部システムとの連携 |
| UIとの連携テスト | TypeScript | フロントエンドとの統合 |
| 複雑なシナリオ | TypeScript | 柔軟な制御が可能 |
| イベントログ解析 | TypeScript | 詳細な解析ツール |
5. 実装例の比較
5.1 初期値のテスト
Solidityでの実装:
function test_InitialValue() public view {
require(counter.x() == 0, "Initial value should be 0");
}
TypeScriptでの実装:
it("Should have initial value of 0", async function () {
const counter = await viem.deployContract("Counter");
const value = await counter.read.x();
expect(value).to.equal(0n);
});
5.2 リバートのテスト
Solidityでの実装:
function test_IncByZero() public {
vm.expectRevert();
counter.incBy(0);
}
TypeScriptでの実装:
it("Should revert when incrementing by 0", async function () {
const counter = await viem.deployContract("Counter");
await expect(counter.write.incBy([0n])).to.be.rejected;
});
5.3 イベントのテスト
Solidityでの実装:
function test_EmitEvent() public {
vm.expectEmit(true, true, true, true);
emit Increment(1);
counter.inc();
}
TypeScriptでの実装:
it("Should emit Increment event", async function () {
const counter = await viem.deployContract("Counter");
await viem.assertions.emitWithArgs(
counter.write.inc(),
counter,
"Increment",
[1n],
);
});
6. テスト環境のセットアップ
6.1 Solidityテストの準備
# Forge標準ライブラリのインストール(既にpackage.jsonに含まれている)
npm install --save-dev forge-std@github:foundry-rs/forge-std#v1.9.4
# テストファイルの作成
touch contracts/MyContract.t.sol
6.2 TypeScriptテストの準備
# Viemツールボックスのインストール(Hardhat 3デフォルト)
npm install --save-dev @nomicfoundation/hardhat-toolbox-viem
# Chaiアサーションライブラリの追加
npm install --save-dev chai @types/chai
# テストファイルの作成
touch test/MyContract.ts
6.3 ハイブリッド環境の設定(Chai + Node:test)
// Hardhat 3でのハイブリッドアプローチ
import { expect } from "chai"; // Chaiのアサーション
import { describe, it } from "node:test"; // Node.jsテストランナー
import { network } from "hardhat"; // Hardhatネットワーク
// これにより、Chaiの豊富なアサーションとNode.jsの標準テストランナーを組み合わせられる
7. テストの実行方法
7.1 全テストの実行
# Solidityテストと TypeScriptテストの両方を実行
npx hardhat test
実行結果の例:
Compiling your Solidity contracts...
Running Solidity tests
contracts/Counter.t.sol:CounterTest
✔ test_InitialValue()
✔ test_IncByZero()
✔ testFuzz_Inc(uint8) (runs: 256)
3 passing
Running node:test tests
Counter
✔ Should emit the Increment event when calling the inc() function
✔ The sum of the Increment events should match the current value
2 passing (1913ms)
7.2 特定のテストファイルの実行
# TypeScriptテストのみ
npx hardhat test test/Counter.ts
# Solidityテストのみ
npx hardhat test contracts/Counter.t.sol
7.3 package.jsonのスクリプト設定
{
"scripts": {
"test": "hardhat test",
"test:solidity": "hardhat test contracts/*.t.sol",
"test:typescript": "hardhat test test/*.ts",
"test:coverage": "hardhat coverage"
}
}
8. ベストプラクティス
8.1 テスト戦略の選択
8.2 テストの組み合わせ方
- コアロジック: Solidityテストで基本機能を高速に検証
- エッジケース: ファズテストで予期しない入力を検証
- 統合動作: TypeScriptテストで実際の使用シナリオを検証
- パフォーマンス: Solidityテストでガス使用量を最適化
8.3 テストの命名規則
Solidityテスト:
-
test_FunctionName_Condition: 通常のテスト -
testFuzz_FunctionName: ファジングテスト -
testFail_FunctionName_Reason: 失敗を期待
TypeScriptテスト:
Should [expected behavior] when [condition]Should revert when [invalid condition]Should emit [EventName] event when [action]
9. トラブルシューティング
9.1 よくあるエラーと解決方法
Solidityテストのエラー:
Error: setUp() failed
解決: setUp()関数内のコントラクトデプロイを確認
TypeScriptテストのエラー:
ReferenceError: describe is not defined
解決: import { describe, it } from "node:test"を追加
Mochaを使用したい場合のエラー:
Unknown file extension ".ts"
解決: Hardhat 3のデフォルトはNode.jsテストランナー。Mochaプラグインを追加するか、Node.jsテストランナーを使用
9.2 デバッグテクニック
Solidityテスト:
// console.logの使用
import "hardhat/console.sol";
function test_Debug() public {
console.log("Value:", counter.x());
}
TypeScriptテスト:
// 詳細なログ出力
console.log("Contract address:", counter.address);
console.log("Transaction hash:", txHash);
console.log("Gas used:", receipt.gasUsed);
10. 高度なテスト技法
10.1 ファズテストの活用
Solidityでのファズテスト:
function testFuzz_ComplexScenario(
uint256 amount,
address user,
bytes32 data
) public {
vm.assume(amount > 0 && amount < 1e18);
vm.assume(user != address(0));
// テストロジック
}
TypeScriptでのプロパティベーステスト:
import fc from 'fast-check';
it("Should handle random inputs", async () => {
await fc.assert(
fc.asyncProperty(fc.nat(), async (value) => {
const result = await counter.write.incBy([BigInt(value)]);
// アサーション
})
);
});
10.2 ガスレポート
# ガス使用量のレポート生成
REPORT_GAS=true npx hardhat test
10.3 カバレッジ測定
# テストカバレッジの測定
npx hardhat coverage
11. 学習の成果
11.1 習得したスキル
- Solidityテストの記述: Foundry/Forge形式でのテスト実装
- TypeScriptテストの記述: Viem/Chaiを使用したテスト実装
- テストランナーの理解: Node.jsテストランナーとMochaの違い
- ハイブリッドアプローチ: 両方のテスト手法の組み合わせ
- アサーションライブラリ: Chaiの活用方法
11.2 重要な学び
- 適材適所: テストの目的に応じて適切な手法を選択
- 相補的な関係: SolidityテストとTypeScriptテストは補完関係
- 効率的なテスト: 基本機能はSolidity、統合テストはTypeScript
- 継続的な検証: 両方のテストを CI/CD パイプラインに組み込む
12. 今後の展開
12.1 次のステップ
- テストカバレッジの向上: 100%カバレッジを目指す
- CI/CD統合: GitHub Actionsでの自動テスト
- ミューテーションテスト: テストの品質検証
- 形式検証: 数学的証明によるコントラクト検証
12.2 応用分野
- DeFiプロトコル: 複雑な金融ロジックのテスト
- NFTコントラクト: ミント、転送、権限管理のテスト
- DAO: ガバナンス機能のテスト
- Bridge: クロスチェーン機能のテスト
12.3 推奨リソース
- Foundry Book: Solidityテストの詳細
- Viem Documentation: TypeScriptテストのAPI
- Hardhat Testing: Hardhatのテストガイド
- Chai Assertion Library: アサーションの詳細
まとめ:
Hardhatにおけるテストは、SolidityテストとTypeScriptテストの両方を活用することで、包括的で信頼性の高いスマートコントラクト開発が可能になります。それぞれの長所を理解し、プロジェクトの要求に応じて適切に使い分けることが重要です。
Discussion