Hardhat v3:Solidity Test vs TypeScript Test、実測で2.7倍の速度差を検証
はじめに
Hardhat v3 では、Solidity で書くテスト(.t.sol)と、TypeScript(node:test + viem)で書くテストの 2 つを選べるようになりました。
便利な反面、「結局どっちが速い?」「CI が遅いときに効くのはどっち?」「用途別にどう使い分ける?」と迷う場面も増えたと思います。
本記事では、同一の Counter テスト(100 回 increment して最終値を検証)を題材に、両方式の実行時間を hyperfine で実測し、速度差が出る理由と 使い分けの目安を整理します。
※本記事の比較は、Solidity 側が EVM 内で完結するのに対し、TypeScript 側は
await counter.write.inc()を 100 回呼ぶため100 tx 分の往復(RPC + await)を含みます。
つまり「純粋な計算速度」だけでなく、現実の開発体験に近いオーバーヘッド差も含んだ比較です(1 tx に揃えた参考も後半に記載します)。
結論
Warm run(100 runs)で計測したところ、Hardhat v3 の Solidity Test(.t.sol) は
TypeScript Test(node:test + viem) より 約 2.7〜2.8 倍高速でした。
- Solidity Test: 0.538 s ± 0.016 s
- TypeScript Test: 1.480 s ± 0.023 s
差の主因は、Solidity Test が EVM 上でテストロジック(loop / assertion)まで完結する一方で、TypeScript Test は JSON-RPC 往復 + await を繰り返す(今回の例では 100 tx)ためです。
なお、TypeScript 側の処理を 1 tx にまとめられる設計(例:incBy(100))では、この比率は変わり得ます(後述)。
実測結果サマリ
Warm run(100 runs)
| テスト方式 | 実行時間(mean ± σ) | 相対速度 |
|---|---|---|
| Solidity Test (.t.sol) | 537.7 ms ± 16.2 ms | 1.00x |
| TypeScript Test (node:test + viem) | 1.480 s ± 0.023 s | 約 2.75x 遅い |
- 事前に
hardhat buildを実行し、コンパイル時間の影響を除外 -
hyperfine --warmup 1でウォームアップ後の結果を採用
実験環境
- OS: macOS(Darwin 24.5.0)
- Machine: MacBook Pro(Apple Silicon)
- Chip: Apple M4 Max
- CPU: 14 cores(10 performance + 4 efficiency)
- Memory: 36 GB
- Node.js: v22.21.1
- Hardhat: v3.1.0
- Solidity: 0.8.28
- EVM target: Cancun
- Benchmark tool: hyperfine
前提(TypeScript テストを動かすために)
Hardhat v3 の Solidity Test は built-in なので追加インストール不要です。
一方、npx hardhat test nodejs(node:test で TypeScript テストを走らせるタスク)は
@nomicfoundation/hardhat-node-test-runner により提供されます。
-
hardhat --initで “viem + node:test” ベースのプロジェクトを作った場合は、基本的に必要なものが揃っています - もし
test nodejsが見つからない場合は、toolbox を入れるのが手っ取り早いです
npm add --save-dev @nomicfoundation/hardhat-toolbox-viem
import { defineConfig } from "hardhat/config";
import hardhatToolboxViem from "@nomicfoundation/hardhat-toolbox-viem";
export default defineConfig({
plugins: [hardhatToolboxViem],
});
すでに別構成で運用している場合は、必要なプラグインだけを個別に入れる方法もあります(公式ガイド参照)。
再現用の最小構成
今回の検証は、以下の最小構成で再現できます。
.
├── contracts/
│ ├── Counter.sol
│ └── Counter.t.sol # Solidity Test(.t.sol)
├── test/
│ └── counter.test.ts # TypeScript Test(node:test + viem)
└── hardhat.config.ts
何を比較しているか(先に前提)
Hardhat v3 には(少なくとも)次の 2 パターンのテストがあります。
-
Solidity Test(.t.sol)
テストロジック(loop / assertion 含む)が EVM 上で完結する -
TypeScript Test(node:test + viem)
テストロジックは Node.js 側で実行され、EVM とは JSON-RPC 経由で通信する
今回の TypeScript テストは inc() を 100 回 await しているため、
- Node.js の実行
- JSON-RPC の往復
- 100 回の tx 送信・実行(ローカルでも “チェーンを 100 回進める”)
といったオーバーヘッドも含めて「実際のテスト実行時間」を比較しています。
使用したコントラクト(共通)
ベンチマーク目的のため、意図的にシンプルな Counter です。
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;
contract Counter {
uint256 public x;
function inc() public {
x += 1;
}
}
Solidity Test(.t.sol)
Solidity で直接テストを書くパターンです。
counter.inc() の loop も assertion も Solidity(EVM)側で完結します。
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;
import { Counter } from "./Counter.sol";
contract CounterSolidityTest {
Counter counter;
function setUp() public {
counter = new Counter();
}
function test_increment_100_times() public {
for (uint256 i = 0; i < 100; i++) {
counter.inc();
}
require(counter.x() == 100, "x should be 100");
}
}
補足:Hardhat は
contracts/*.t.sol、またはtest/ディレクトリ配下の Solidity をテストとして扱います。
さらにtestで始まる public 関数がテストとして実行され、revert すると fail になります。
setUp()は各テストの前に呼ばれます(Foundry ライクな感覚)。
TypeScript Test(node:test + viem)
node:test + viem パターンです。
await counter.write.inc() を 100 回呼ぶため、(ローカルでも)tx 実行が 100 回発生します。
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import hre from "hardhat";
const { viem } = await hre.network.connect();
describe("Counter (TypeScript test)", () => {
it("increment 100 times", async () => {
const counter = await viem.deployContract("Counter");
for (let i = 0; i < 100; i++) {
await counter.write.inc();
}
const value = await counter.read.x();
assert.equal(value, 100n);
});
});
ベンチマーク方法
- 事前にコントラクトをコンパイル(
hardhat build) -
hyperfineでそれぞれ 100 回実行(warmup あり) - mean(平均)と σ(標準偏差)を比較
# 事前コンパイル(コンパイル時間を除外するため)
npx hardhat build
# Solidity Test(100 runs)
hyperfine --warmup 1 --runs 100 'npx hardhat test solidity'
# TypeScript Test(100 runs)
hyperfine --warmup 1 --runs 100 'npx hardhat test nodejs'
<details>
<summary>hyperfine output(抜粋)</summary>
※雰囲気が伝わるように、代表的な行だけ抜粋しています(平均と標準偏差)
- Solidity: mean 0.538 s, stddev 0.016 s
- NodeJS: mean 1.480 s, stddev 0.023 s
</details>
注意点(この結果が変わり得る条件)
今回の “2.7 倍差” は再現性がある一方で、次の要因で比率が変動し得ます。
-
tx 回数が増えるほど、TypeScript 側(RPC 往復 + await ループ)が相対的に不利になりやすい
(今回はinc()を 100 回 = 100 tx) - 逆に、TypeScript 側で 1 tx にまとめられる設計(例:
incBy(100))だと差は縮む可能性が高い - テストケース数が増えると、CLI 起動や初期化などの固定オーバーヘッドの比率が変わる
- Node.js / Hardhat / plugin のバージョン、CPU、電源モード等でも変わり得る
(参考)“1 tx” に揃えた比較の書き方
「それは 100 tx だから遅いだけでは?」という視点も正しいので、
“1 tx に揃える” 比較を Appendix として用意しておくと議論がスムーズになります。
例えば、コントラクトに incBy(uint256) を生やして 1 回の call にまとめます。
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;
contract Counter {
uint256 public x;
function inc() public {
x += 1;
}
function incBy(uint256 by) public {
x += by;
}
}
Solidity Test 側(1 tx 相当の call にまとめる)
function test_incBy_100() public {
counter.incBy(100);
require(counter.x() == 100, "x should be 100");
}
TypeScript Test 側(1 tx にまとめる)
await counter.write.incBy([100n]);
const value = await counter.read.x();
assert.equal(value, 100n);
この “1 tx 版” は、今回のメイン結果(100 tx を含む実測)とは別物なので、数値が変わっても不思議ではありません。
「何を速くしたいか」(EVM 内の検証? 開発時の end-to-end テスト?)を切り分けるための参考です。
なぜこれほど差が出るのか
理由はシンプルで、実行レイヤーの違いです。
Solidity Test が速い理由
- テストの loop / assertion / コントラクト呼び出しが EVM 上で完結
- JSON-RPC の往復がない
- Node 側の
awaitループがない
TypeScript Test が遅くなる理由(今回の書き方の場合)
- テストロジックが Node.js 側で動く
-
counter.write.inc()は tx 送信であり、100 回分の JSON-RPC 往復が発生する -
awaitによるコンテキスト切り替えが 100 回発生する
つまり「Solidity が速い」というより、EVM に近い場所で完結するほど速いという話です。
TypeScript Test は不要になるのか?
結論は No です。用途が違います。
| 用途 | 推奨 |
|---|---|
| コアロジック / 低レイヤの高速検証 | Solidity Test |
| 複雑なシナリオ(複数ユーザー / 時間操作 / 外部連携) | TypeScript Test |
| フロントエンド連携・E2E 寄りの検証 | TypeScript Test |
| 高速な CI(まず速く落とす) | Solidity Test を厚めに |
Hardhat v3 の良さは、テスト戦略の選択肢が増えることだと思っています。
まとめ
- Hardhat v3 の Solidity Test(
.t.sol)は、今回の条件(100 回 increment)では TypeScript(node:test + viem)より約 2.7〜2.8 倍高速だった - 速度差の本質は 実行レイヤーの違い
- Solidity Test:EVM 内で完結(RPC/await がない)
- TypeScript Test:Node 側でループし、RPC 往復 + await(今回 100 tx)が乗る
- ただし TypeScript Test が不要になるわけではなく、複雑なシナリオテストやフロント連携/E2E では依然重要
- 速度がボトルネックなら、ユニットテスト寄りは Solidity Test を厚めに、統合寄りは TypeScript Test、というハイブリッドが現実的
参考リンク
- Writing unit tests in Solidity(Hardhat 3 Docs)
- Testing your smart contracts with viem and node:test(Hardhat 3 Docs)
DG Technology本部はデジタルガレージ全体の技術導入とシステム開発に責任を持ち、部署を横断して各プロジェクトやプロダクトの設計、開発、運用を担当する組織です。 ※原則としてコメントへの返信対応は行なっていません。 tech.garage.co.jp/dgt/
Discussion