⚡️

Hardhat v3:Solidity Test vs TypeScript Test、実測で2.7倍の速度差を検証

に公開
2026/09/01

はじめに

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
hardhat.config.ts
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 です。

contracts/Counter.sol
// 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)側で完結します。

contracts/Counter.t.sol
// 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 回発生します。

test/counter.test.ts
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);
  });
});

ベンチマーク方法

  1. 事前にコントラクトをコンパイル(hardhat build)
  2. hyperfine でそれぞれ 100 回実行(warmup あり)
  3. 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 にまとめます。

contracts/Counter.sol
// 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 にまとめる)

contracts/Counter.t.sol
function test_incBy_100() public {
    counter.incBy(100);
    require(counter.x() == 100, "x should be 100");
}

TypeScript Test 側(1 tx にまとめる)

test/counter.test.ts
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、というハイブリッドが現実的

参考リンク

デジタルガレージ DG Technology本部

Discussion