単体テストのモック作成が圧倒的に楽になる。TypeScriptでクラスを使わないモジュール設計

TypeScriptのクラスレス設計で単体テストとモックが簡単になる構造図 アーキテクチャ

単体テストにおいてモックの作成が煩雑になる原因は、設計の段階でテスト容易性が十分に考慮されていないことに起因する場合が多いです。
特にTypeScriptでクラス中心の設計を採用していると、依存関係の差し替えが困難になり、結果としてモック生成が複雑化しがちです。

この問題は単なるテスト技法の工夫では解決しきれず、モジュール設計そのものを見直す必要があります。
例えば、関数ベースのモジュール構成へ移行することで、依存を明示的に引数として扱うことができ、テスト時の差し替えが容易になります。

実務でよく見られる課題としては以下のようなものがあります。

  • クラス依存による差し替えの難しさ
  • インスタンス生成の隠蔽によるテスト不能領域の発生
  • テスト環境と本番環境の境界の曖昧さ

これらの問題は、設計の初期段階でクラスを使わない設計思想を取り入れることで大幅に軽減できます。
特に関数を中心としたモジュール構成では、入力と出力が明確になり、副作用も制御しやすくなるため、単体テストにおけるモックの必要性自体を最小限に抑えることが可能です。

本記事では、TypeScriptにおける実践的なモジュール設計を通じて、単体テストの複雑さをどのように削減できるのかを論理的に解説していきます。

単体テストでモック作成が難しくなるTypeScript設計の問題点

単体テストとモックの複雑さを示すコード設計のイメージ

単体テストにおいてモック作成が困難になる原因は、テスト技法そのものではなく、設計上の前提に起因するケースが多いです。
特にTypeScriptにおいてクラスベースの設計を採用すると、依存関係が暗黙的に生成・保持される構造になりやすく、結果としてテスト時に差し替え可能な境界が曖昧になります。

この問題は「テストが難しいコード」という表層的な現象ではなく、「テストしづらい構造を持った設計」が原因です。
そのため、モックを無理に作ることで対処しようとすると、テストコード自体が肥大化し、保守性が低下します。

代表的な問題点は以下の通りです。

  • コンストラクタ内で依存インスタンスを生成しているため差し替えが困難
  • privateメソッドや内部状態に依存するテストが増加する
  • クラスの責務が肥大化し、単体テストの粒度が不明確になる

これらの問題は、設計初期の段階で「依存性を外から注入できるか」という観点が欠落している場合に顕在化します。
特に、new演算子による直接生成が多用されているコードベースでは、テスト環境での置き換えが困難になり、結果としてモックフレームワークへの依存度が高くなります。

また、TypeScriptの型システムは構造的型付けであるため、一見するとモックは容易に見えます。
しかし実際には、クラスの内部状態や副作用が絡むことで、型だけでは表現できない依存関係が生まれます。
このギャップが、テスト設計の複雑さを増幅させる要因となります。

さらに問題を複雑にする要素として、以下のような実装パターンが挙げられます。

問題パターン 影響 結果
直接インスタンス生成 依存差し替え不可 モック困難化
グローバル依存参照 テスト間干渉 再現性低下
状態保持クラス 初期化コスト増大 テスト遅延

これらの構造が積み重なることで、単体テストは本来の目的である「ロジック検証」から逸脱し、「環境構築とモック調整」が主目的になってしまうことがあります。

重要なのは、モックを簡単にすることではなく、モックが不要な構造に設計を寄せることです。
そのためには、クラス中心の設計を前提とせず、依存関係を明示的に引数として扱う設計へ移行する必要があります。
この視点が欠けると、テストは常に外部構造に引きずられることになります。

結果として、TypeScriptにおける単体テストの難しさは、言語仕様ではなく設計思想の選択に強く依存していると整理できます。

クラスベース設計がテスト容易性を下げる理由と依存関係の罠

クラス設計と依存関係の複雑さを示す抽象的な図

クラスベース設計は一見すると構造化されており、責務分離や再利用性の観点でも優れているように見えます。
しかし単体テストという観点に限定すると、その構造が逆に足かせとなり、テスト容易性を大きく損なうケースが多く見られます。
特にTypeScriptのように柔軟な型システムを持つ言語では、この問題が顕在化しやすい傾向があります。

本質的な問題は、クラスが「状態」と「振る舞い」を同時に内包することにあります。
状態を持つことで依存関係が内部に固定されやすくなり、外部からの制御が難しくなるため、テスト時にモックへ置き換えるための接点が不足します。
結果として、テストコードが本来触れるべきでない内部構造に依存してしまう構造が生まれます。

この問題を整理すると、以下のような特徴に分解できます。

  • 依存オブジェクトの生成がクラス内部に隠蔽される
  • コンストラクタが肥大化し、差し替え可能性が低下する
  • インスタンス状態にテストが依存し、再現性が低下する

これらは単独で発生するのではなく、相互に影響し合いながらテスト設計を複雑化させます。
特に厄介なのは「依存オブジェクトの生成がクラス内部に隠蔽される」パターンです。
この場合、外部からの注入ができないため、テスト時には実際の実装をそのまま利用するか、強引なモック差し替えを行う必要が出てきます。

例えば以下のような構造を考えます。

class UserService {
  private repo = new UserRepository();
  getUser(id: string) {
    return this.repo.find(id);
  }
}

この設計では UserRepository が内部で生成されているため、テスト時に差し替える余地がありません。
その結果、実際のDB接続や外部依存に引きずられるテストになりやすく、単体テストとしての独立性が失われます。

さらに問題を複雑にするのが、状態を持つクラス設計です。
インスタンス内部にキャッシュやフラグが存在する場合、テストの順序や初期化条件によって結果が変わることがあり、これは再現性の観点で重大な欠陥となります。
単体テストは本来、同一入力に対して常に同一出力を保証すべきですが、状態依存が強い設計ではこの前提が崩れます。

また、クラス設計では責務が曖昧になりやすいという問題もあります。
メソッドが増えるにつれて「どの振る舞いがどの依存に依存しているのか」が不明瞭になり、結果としてテスト対象の粒度が肥大化します。
これはテストの目的である「最小単位でのロジック検証」と矛盾します。

テスト容易性の観点から整理すると、クラスベース設計には以下のようなトレードオフがあります。

観点 メリット デメリット
構造化 責務がまとまりやすい 依存が内部に閉じる
再利用性 継承による拡張 テスト時の柔軟性低下
カプセル化 内部状態の保護 モック困難化

このように、クラス設計はプロダクションコードでは有効に機能する一方で、テストコードとの相性が必ずしも良いとは限りません。
特に単体テストでは「差し替え可能性」が重要な設計要件となるため、内部生成や状態保持は構造的な制約として働きます。

結果として、依存関係がクラス内部に閉じている設計では、テストは常に実装詳細に引きずられ、モックの複雑化やテストの脆弱性を招きます。
この構造的問題を理解せずにモック技術だけを改善しようとしても、本質的な解決には至りません。

TypeScriptにおける依存性注入とモックの限界

依存性注入とテストモックの関係を示す概念図

依存性注入(Dependency Injection)は、単体テストにおけるモック容易性を高めるための代表的な設計手法です。
TypeScriptにおいても、コンストラクタや関数引数を通じて依存を外部から渡すことで、実装の差し替えが可能になり、テストの独立性を確保できます。
しかし実務レベルで運用すると、この仕組みだけでは解決できない構造的な限界が見えてきます。

まず依存性注入の基本的な効果は明確です。
依存を外部化することで、以下のようなメリットが得られます。

  • テスト時にモックへ容易に差し替え可能
  • コンポーネントの再利用性が向上
  • 依存関係が明示的になり可読性が向上

このように、理論上は非常に強力な設計パターンです。
しかしTypeScriptの現場では、依存性注入を導入してもなおモックが複雑になるケースが少なくありません。
その理由は、依存の「型」ではなく「構造」と「副作用」に起因する問題が残るためです。

特に問題となるのは、以下のようなケースです。

  • 依存がインターフェースではなく具体クラスに強く結びついている
  • 複数依存の組み合わせによりモック構築が過剰に複雑化する
  • 外部APIやI/O処理などの副作用が依存チェーンの深層に存在する

例えば次のような設計を考えます。

class OrderService {
  constructor(
    private paymentGateway: PaymentGateway,
    private inventoryService: InventoryService
  ) {}
  async createOrder(order: Order) {
    await this.inventoryService.reserve(order.items);
    return this.paymentGateway.charge(order.amount);
  }
}

このように依存性注入を行っていても、テスト時には PaymentGatewayInventoryService の両方をモックする必要があり、さらにそれぞれが内部で別の依存を持っている場合、モック構築は急速に複雑化します。

この問題は単なる「モックの数が増える」という話ではありません。
より本質的には、依存グラフが深くなることでテストの認知負荷が増大する点にあります。
依存が2〜3層程度であれば管理可能ですが、実務のコードベースでは5層以上の依存チェーンが発生することも珍しくありません。

ここで依存性注入の限界が現れます。
それは「依存を外に出すことはできても、依存構造の複雑さそのものは解消できない」という点です。
つまりDIは構造を透明化する手段であって、構造を単純化する手段ではありません。

さらにTypeScript特有の問題として、型システムが依存の複雑さを隠蔽してしまう点があります。
型定義上は単純に見える依存関係でも、実際にはランタイムで多段階の初期化や副作用が発生していることがあり、テスト時に初めてその複雑さが露呈します。

この状況を整理すると、依存性注入とモックには以下のような限界があります。

観点 期待される効果 実際の制約
依存の明示化 テスト容易性向上 依存数増加で複雑化
モック可能性 差し替え容易 モック構築コスト増大
設計の単純化 構造の透明化 副作用は残存

重要なのは、依存性注入を導入したとしても、設計そのものが複雑であればテストは必ず複雑になるという事実です。
DIはあくまで「差し替え可能性を提供する仕組み」であり、「複雑さを消す仕組み」ではありません。

そのため実務では、依存性注入に加えて「依存そのものを減らす設計」が不可欠になります。
特に副作用を持つ処理を境界層に隔離し、コアロジックを純粋関数として扱う設計に寄せることで、初めてモック依存から脱却しやすくなります。

結果として、TypeScriptにおける依存性注入の価値は「万能な解決策」ではなく、「設計改善の一部手段」として位置づけるのが適切です。

クラスを使わないモジュール設計の基本思想とメリット

関数ベースのモジュール設計でシンプルに構成されたコード概念

クラスを前提としないモジュール設計は、TypeScriptにおける設計思想を根本から見直すアプローチです。
この設計では「状態を持つオブジェクト」を中心に据えるのではなく、「入力と出力が明確な関数」を基本単位としてシステムを構築します。
その結果、依存関係は自然と外部から注入される形となり、テスト容易性が大幅に向上します。

この思想の核心は、振る舞いをクラスではなく関数の合成として捉えることにあります。
クラスは状態と振る舞いを密結合させるため、内部状態が増えるほど複雑性が増大します。
一方で関数ベースの設計では、状態を極力外部に追い出すことで、純粋なロジック単位へと分解できます。

この設計がもたらす代表的なメリットは次の通りです。

  • 依存関係が関数引数として明示化されるため追跡が容易
  • 状態を持たないためテストの再現性が高い
  • モジュール単位での差し替えが容易
  • モックの必要性そのものが減少する

これらの利点は単なるコードの簡潔さではなく、システム全体の認知負荷を下げる点にあります。
特に大規模なTypeScriptプロジェクトでは、依存関係の複雑さが開発速度を低下させる主要因となるため、関数中心設計の効果は顕著に現れます。

例えば、クラスベース設計では以下のような構造になりがちです。

class PriceCalculator {
  constructor(private taxRate: number) {}
  calculate(price: number) {
    return price + price * this.taxRate;
  }
}

これに対してクラスを排除した設計では、次のように表現できます。

const createPriceCalculator = (taxRate: number) => {
  return (price: number) => price + price * taxRate;
};

この違いは表面的には小さく見えますが、テスト観点では大きな差を生みます。
後者は依存(taxRate)が明示的に引数として渡されているため、モックや差し替えの必要がほぼ消失します。
また関数単位でのテストが可能になるため、ユニットの粒度が自然と最小化されます。

さらに重要なのは、関数ベース設計では「状態の寿命」を明示的に制御できる点です。
クラスではインスタンスが生成された時点から破棄されるまで状態が保持されますが、関数設計では必要なタイミングでのみ状態を生成できます。
この違いは並行処理や非同期処理において特に重要です。

観点 クラス設計 関数ベース設計
状態管理 インスタンス内部に保持 呼び出しごとに生成
依存関係 隠蔽されやすい 明示的
テスト容易性 低い傾向 高い傾向
モック必要性 高い 低い

また、関数ベース設計はTypeScriptの型システムとも相性が良いという特徴があります。
構造的型付けにより、関数の入力と出力が明確であれば、自然にインターフェースとして成立するため、追加の抽象化を必要としません。

重要なのは、この設計が単なる「クラスを使わないスタイル」ではなく、「依存を外に出し、状態を制御可能にする設計思想」であるという点です。
その結果として、単体テストのモック依存度が低下し、テストコード自体の複雑性も大幅に削減されます。

このように、クラスを排除することは目的ではなく、結果としてテスト容易性と保守性を高めるための手段であると位置づけることが重要です。

関数型アプローチで実現するテスト容易なTypeScript設計

関数型設計でテストしやすい構造を表した図

関数型アプローチを採用したTypeScript設計は、単体テストの容易性という観点で非常に合理的な構造を提供します。
その理由は明確で、状態を持たず、入力と出力が明示された関数を中心にシステムを構築することで、テスト対象が極めて予測可能になるためです。
クラスベース設計に見られるような内部状態や依存の隠蔽が排除されることで、テストは本質的に「純粋なロジック検証」に収束します。

このアプローチの本質は、副作用を制御可能な境界に閉じ込めることです。
すべての副作用を関数の外側、もしくは明示的な引数として扱うことで、関数自体は常に同じ入力に対して同じ出力を返す構造になります。
この性質はテスト容易性に直結します。

関数型設計における基本的な特徴は以下の通りです。

  • 状態を持たない純粋関数を基本単位とする
  • 依存関係は引数として明示的に渡される
  • 副作用は境界層に隔離される
  • 合成によって複雑な処理を構築する

これらの特徴により、テストは非常に単純化されます。
例えば、以下のような関数を考えます。

const calculateDiscount = (price: number, rate: number) => {
  return price - price * rate;
};

この関数は外部依存を一切持たないため、モックは不要であり、入力と出力のみを検証すれば十分です。
単体テストは次のように極めて単純になります。

test("calculateDiscount", () => {
  expect(calculateDiscount(1000, 0.1)).toBe(900);
});

このような設計では、テストの複雑さは関数の複雑さに比例します。
つまり、設計そのものがテスト難易度を直接決定する構造になっています。

さらに重要なのは、関数型アプローチでは「依存の注入」ではなく「依存の合成」が行われる点です。
これにより、依存関係は静的ではなく動的に構築され、テスト時には必要な部分だけを差し替えることが可能になります。

例えば次のような構造です。

const createOrderProcessor = (
  payment: (amount: number) => boolean,
  logger: (msg: string) => void
) => {
  return (amount: number) => {
    const result = payment(amount);
    logger(`payment result: ${result}`);
    return result;
  };
};

この設計では、paymentlogger を自由に差し替えることができるため、テスト時には以下のようなモック関数を簡単に注入できます。

  • 成功・失敗を制御するpaymentモック
  • ログ出力を無効化するダミーlogger

これにより、テストは完全に制御された環境下で実行でき、再現性が極めて高くなります。

関数型設計の利点を整理すると以下のようになります。

観点 関数型設計 クラス設計
状態管理 なし(明示的) インスタンス内部
副作用制御 境界で分離 内部に混在
テスト容易性 非常に高い 低い傾向
モック依存度 低い 高い

また、関数型設計はTypeScriptの型システムと自然に統合されます。
関数の型シグネチャそのものが契約となるため、追加の抽象化やフレームワークに依存する必要がありません。
この点は長期的な保守性にも直結します。

重要なのは、関数型アプローチは単なるスタイルではなく、「依存と状態をどこに置くか」という設計上の意思決定であるという点です。
この意思決定によって、テスト容易性は構造的に保証されるようになります。

結果として、関数型設計は単体テストのための補助技術ではなく、テスト容易性そのものを内包した設計手法であると評価できます。

JestやVitestでのモック作成が劇的にシンプルになる実践例

テストフレームワークでモックが簡単になるコード例のイメージ

単体テストにおけるモックの複雑さは、テストフレームワークの問題というよりも、設計と依存関係の持ち方に強く依存します。
そのため、JestやVitestといったモダンなテストランナーを用いていても、クラスベースで依存が内部生成されている設計ではモックは依然として煩雑になります。
一方で関数ベースのモジュール設計に移行すると、モックは驚くほど単純化され、テストコードの可読性と保守性が大幅に向上します。

この章では、JestおよびVitestにおけるモック作成の実践例を通して、設計がどのようにテスト容易性に影響するかを論理的に整理します。

まず、外部依存を持つ典型的なユースケースとして、ユーザー情報を取得するAPI呼び出しを考えます。

export const fetchUser = async (id: string) => {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
};

このような関数は純粋なI/O依存を持つため、テスト時にはネットワークアクセスを避ける必要があります。
Jestでは以下のようにグローバル関数をモックすることで対応できます。

global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve({ id: "1", name: "Alice" })
  })
) as jest.Mock;

Vitestでも同様に以下のような形でモック可能です。

import { vi } from "vitest";
global.fetch = vi.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve({ id: "1", name: "Alice" })
  })
);

この時点でも十分にシンプルに見えますが、依存が関数の内部に隠蔽されている場合、モックの難易度は一気に上昇します。
特にクラス内部でfetchや外部クライアントを生成している場合、テストはその内部構造に依存せざるを得なくなります。

ここで重要なのが、依存を外部から注入可能な関数設計への移行です。
例えば以下のように設計を変更します。

export const createUserService = (fetcher: typeof fetch) => {
  return {
    getUser: async (id: string) => {
      const res = await fetcher(`/api/users/${id}`);
      return res.json();
    }
  };
};

この構造にすると、テスト時のモックは非常に単純になります。

import { createUserService } from "./userService";
const mockFetch = vi.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve({ id: "1", name: "Alice" })
  })
);
test("getUser returns user data", async () => {
  const service = createUserService(mockFetch as any);
  const user = await service.getUser("1");
  expect(user.name).toBe("Alice");
});

このように依存を外部化することで、モック対象は「グローバルAPI」ではなく「単なる関数」に変わります。
この変化がテスト設計に与える影響は非常に大きく、以下のような利点が生まれます。

  • モック対象が明確になるためテストの理解コストが低下する
  • グローバル状態への依存が排除されるためテストの独立性が向上する
  • テストのセットアップが短くなり可読性が向上する

さらに重要なのは、モックの粒度が自然に小さくなる点です。
クラスベース設計ではインスタンス単位でのモックが必要になるため、不要なメソッドまで巻き込むケースが多発します。
しかし関数ベース設計では必要な依存だけを差し替えればよいため、モックは最小単位で構築できます。

また、VitestやJestのモック機能は本来非常に強力ですが、その真価は「差し替え可能な設計」と組み合わせて初めて発揮されます。
設計が不適切な場合、どれほど強力なモック機構を持っていても複雑性は解消されません。

以下に構造の違いを整理します。

観点 クラスベース設計 関数ベース設計
モック対象 インスタンス全体 個別関数
初期化コスト 高い 低い
テストの独立性 低下しやすい 高い
可読性 依存により低下 明示的で高い

結論として、JestやVitestのモックを簡単にする本質的な方法は、フレームワークの使い方ではなく、依存を関数として外部化する設計にあります。
この設計変更により、モックは複雑な制御手段から単純な差し替え手段へと変化し、単体テストの本質である「ロジックの検証」に集中できるようになります。

実務で使えるクラスレス設計へのリファクタリング手順

レガシーコードから関数モジュールへ移行する流れの図

クラスレス設計へのリファクタリングは、単に「クラスを関数に置き換える」作業ではありません。
本質的には、依存関係と状態管理の責務を再配置し、テスト容易性と保守性を構造的に改善するプロセスです。
特に既存のTypeScriptプロジェクトでは、クラス中心の設計が長期間積み重なっているため、段階的かつ論理的な移行が必要になります。

このリファクタリングの目的は明確で、副作用と状態をコアロジックから分離することです。
これにより、単体テストでのモック依存度を下げ、コードの見通しを改善します。

まず最初に行うべきは、依存関係の棚卸しです。
クラス内部で生成されている依存や、暗黙的に利用されている外部サービスをすべて洗い出します。
この段階ではコードの変更は行わず、構造の可視化に集中します。

典型的なチェック項目は以下の通りです。

  • コンストラクタ内でnewされている依存の特定
  • グローバル変数やシングルトンの利用箇所
  • メソッド内部でのI/O処理やAPI呼び出し
  • 状態を変更する副作用の発生箇所

次に行うのは、依存の外部化です。
クラス内で生成されている依存をすべて関数の引数として明示化します。
この段階ではまだクラスを残しても構いませんが、依存の生成だけは必ず外に出します。

例えば以下のようなクラスがあるとします。

class ReportService {
  private api = new ApiClient();
  async fetchReport(id: string) {
    return this.api.get(`/reports/${id}`);
  }
}

これを関数ベースに移行するためには、まず依存を注入可能にします。

class ReportService {
  constructor(private api: ApiClient) {}
  async fetchReport(id: string) {
    return this.api.get(`/reports/${id}`);
  }
}

この時点で既にテスト容易性は改善しますが、さらに関数化することで構造を単純化できます。

const createReportService = (api: { get: (url: string) => Promise<any> }) => {
  return {
    fetchReport: (id: string) => api.get(`/reports/${id}`)
  };
};

この変換により、依存は完全に外部から制御可能となり、テスト時のモック構築が容易になります。

次に行うべきは、副作用の分離です。
API呼び出しやファイルアクセスなどのI/O処理をすべて境界層に移動させ、コアロジックを純粋関数として残します。
この設計により、ビジネスロジックのテストはI/Oから完全に独立します。

リファクタリングの段階を整理すると以下のようになります。

フェーズ 目的 状態
分析 依存関係の可視化 変更なし
外部化 依存の注入可能化 ハイブリッド
関数化 クラス排除 純関数化
分離 副作用隔離 完全分離

さらに重要なのは、一度にすべてを変えないことです。
実務環境ではクラスレス化を段階的に進める必要があります。
特に大規模コードベースでは、部分的なリファクタリングを許容しながら進めることで、システム全体の安定性を維持できます。

また、テストを並行して整備することも重要です。
リファクタリング前後で挙動が変わらないことを保証するため、既存テストを保護網として利用します。
このプロセスにより、安全に構造変更を進めることができます。

最終的に目指すべき状態は、以下のような構造です。

  • 依存はすべて関数引数として明示化
  • 状態は最小限かつ局所的
  • 副作用は境界層に隔離
  • コアロジックは純粋関数として独立

この状態に到達すると、単体テストは極めてシンプルになります。
モックは必要最小限となり、テストコードはロジックの検証に集中できます。

クラスレス設計へのリファクタリングは単なるリファクタリングではなく、テスト戦略そのものを再設計する行為であると理解することが重要です。

CI環境とテスト戦略の最適化による開発効率向上(GitHub Actions活用)

CIパイプラインと自動テスト実行の流れを示す図

CI環境におけるテスト戦略の最適化は、TypeScriptプロジェクトの品質維持と開発速度の両立において極めて重要な要素です。
特にクラスベース設計からクラスレス設計へ移行した場合、単体テストの構造が単純化されるため、CIパイプライン全体の負荷設計も見直す価値が生まれます。

従来のクラス中心設計では、依存関係が複雑であるがゆえにテスト実行時間が長くなりやすく、さらにモックの準備コストがCI実行時間に直結していました。
一方で関数ベースのモジュール設計では、依存が明示化されるためテストの初期化コストが低くなり、結果としてCIの効率も向上します。

CI環境を設計する上で重要な観点は以下の通りです。

  • テストの並列実行可能性
  • モックセットアップの軽量化
  • キャッシュ戦略による依存解決の高速化
  • テスト失敗時の再現性確保

特にGitHub Actionsを利用する場合、ワークフローの設計次第でテスト効率は大きく変わります。
例えば、依存関係のインストールとテスト実行を分離し、キャッシュを適切に活用することで実行時間を大幅に短縮できます。

以下は基本的なCI構成の例です。

name: CI
on:
  push:
    branches: [main]
  pull_request:
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: "npm"
      - name: Install dependencies
        run: npm ci
      - name: Run tests
        run: npm test

この構成はシンプルですが、設計が適切であれば非常に高いパフォーマンスを発揮します。
特に関数ベース設計と組み合わせることで、テスト自体の実行時間が短縮され、CIのボトルネックが解消されます。

さらに重要なのは、テスト戦略そのものをCIに最適化することです。
単体テスト、統合テスト、E2Eテストを適切に分離し、それぞれの実行タイミングを制御することで、無駄な再実行を防ぐことができます。

例えば以下のような戦略が有効です。

  • 単体テストはプルリクエスト時に必ず実行
  • 統合テストはメインブランチマージ時に実行
  • E2Eテストは夜間またはリリース前に実行

このように段階的にテストを分離することで、CIの負荷は大幅に削減されます。

また、クラスレス設計との相乗効果も見逃せません。
依存が関数引数として明示化されているため、テスト環境の構築が容易になり、CI上でのモック生成コストも低下します。
これは結果として、CIの安定性向上にも直結します。

さらに、GitHub Actionsのキャッシュ機能を活用することで、依存インストール時間を短縮できます。
これにより、テスト実行時間の大部分をロジック検証に集中させることが可能になります。

観点 改善前 改善後
テスト構造 クラス依存で複雑 関数中心で単純
CI実行時間 長い傾向 短縮可能
モック管理 複雑 最小化
再現性 不安定 高い

最終的にCI最適化の本質は、ツールの設定ではなく設計にあります。
テスト容易性の高い設計を採用することで、CIは単なる実行環境ではなく、品質保証の自動化基盤として機能します。

そのため、GitHub Actionsの活用は手段であり、目的はあくまで「テストがシンプルに実行できる構造」を作ることにあります。

まとめ:TypeScriptでクラスを排除したモジュール設計の本質

シンプルなモジュール設計とテスト容易性の全体像

TypeScriptにおいてクラスを排除したモジュール設計の本質は、単なる構文上の選好ではなく、ソフトウェア設計における依存関係と状態管理の再定義にあります。
これまでの議論で見てきたように、単体テストの複雑さやモックの煩雑さは、テスト技法そのものではなく、設計構造に強く依存しています。
そのため、設計を変えずにテストだけを改善するアプローチには必然的な限界があります。

クラスベース設計は、オブジェクト指向の文脈では強力な抽象化手段として機能しますが、単体テストという観点では内部状態と依存の隠蔽が問題となります。
特に依存関係がコンストラクタ内部で生成される場合、テスト時の差し替え可能性が著しく低下し、結果としてモックの複雑化を招きます。

これに対して関数ベースのモジュール設計は、依存と状態を明示的に外部化することで、構造そのものを単純化します。
この違いは表面的なスタイルの差ではなく、設計原理の差です。

本記事で一貫して示してきた本質的なポイントは以下に集約されます。

  • 依存関係は隠蔽ではなく明示化すべきである
  • 状態は最小化され、必要なスコープに限定されるべきである
  • 副作用はコアロジックから分離されるべきである
  • テスト容易性は設計段階で決定されるべきである

これらの原則を満たす設計では、単体テストは「環境構築」ではなく「ロジック検証」に集中できるようになります。
これは開発効率の観点でも非常に重要です。

また、関数型アプローチに基づく設計は、TypeScriptの型システムとも高い親和性を持ちます。
関数のシグネチャがそのまま契約として機能するため、追加の抽象レイヤーを必要とせず、コードの意図が明確になります。
この点は長期的な保守性にも直結します。

さらに重要なのは、クラスレス設計が「クラスの代替手段」ではなく、「依存と状態の扱い方を再設計するためのアプローチ」であるという点です。
この視点を欠いたまま表面的にクラスを排除しても、設計上の問題は解決されません。

実務的には、以下のような構造を目指すことが合理的です。

  • コアロジックは純粋関数として実装する
  • I/Oや外部依存は境界層に隔離する
  • 依存は関数引数として注入する
  • 状態は局所的かつ短命に保つ

この構造に到達したとき、単体テストのモックは補助的な存在となり、設計そのものがテスト容易性を担保する状態になります。

最終的に重要なのは、テスト容易性を後付けで実現するのではなく、設計の初期段階から組み込むことです。
TypeScriptにおけるクラスレス設計は、そのための合理的な選択肢の一つであり、特に複雑な依存関係を持つプロジェクトにおいて強い効果を発揮します。

したがって本質的な結論は明確です。
単体テストを改善するためにモック技術を磨くのではなく、モックが最小限で済む構造を設計することこそが、最も持続可能で合理的なアプローチであるということです。

コメント

タイトルとURLをコピーしました