JavaScript | 非同期処理:設計・理解の深化 - モック化戦略

JavaScript JavaScript
スポンサーリンク
  1. 「モック化戦略」ってそもそも何の話?
  2. まずは「何をモックするのか」をはっきりさせる
    1. 非同期でよくモック対象になるもの
  3. 戦略1:依存を「引数として渡す」モック化
    1. fetch をモックしやすい形にする
  4. 戦略2:「インターフェース」を決めてモックを差し替える
    1. fetch 以外にも応用できる形にする
  5. 戦略3:時間をモックする(タイマー・遅延のテスト)
    1. setTimeout を直接使わない形にする
  6. 戦略4:戻り値を「テストしやすい形」にする
    1. 副作用だけの非同期関数はモック地獄になりやすい
  7. 戦略5:モックの「粒度」を意識する
    1. どこまでモックするかを決める
  8. 戦略6:モックを「使い捨て」ではなく「設計の一部」として考える
    1. モックも「API の一種」として設計する
  9. 初心者として「モック化戦略」で本当に意識してほしいこと
  10. まずは「何をモックするのか」をはっきりさせる
    1. 非同期でよくモック対象になるもの
  11. 戦略1:依存を「引数として渡す」モック化
    1. fetch をモックしやすい形にする
  12. 戦略2:「インターフェース」を決めてモックを差し替える
    1. fetch 以外にも応用できる形にする
  13. 戦略3:時間をモックする(タイマー・遅延のテスト)
    1. setTimeout を直接使わない形にする
  14. 戦略4:戻り値を「テストしやすい形」にする
    1. 副作用だけの非同期関数はモック地獄になりやすい
  15. 戦略5:モックの「粒度」を意識する
    1. どこまでモックするかを決める
  16. 戦略6:モックを「使い捨て」ではなく「設計の一部」として考える
    1. モックも「API の一種」として設計する
  17. 初心者として「モック化戦略」で本当に意識してほしいこと

「モック化戦略」ってそもそも何の話?

非同期処理をテストしようとすると、必ずぶつかる壁があります。
それが「外部とのやり取り」です。

サーバーへのリクエスト
データベース
タイマー(setTimeout)
ブラウザの API(localStorage, IndexedDB など)

これらを本物のままテストで使うと、

テストが遅い
テストが不安定(ネットワーク次第)
再現性が低い(たまたま通る・たまたま落ちる)

という地獄になります。

そこで出てくるのが モック(mock) です。
「本物の代わりに、テスト用のニセモノを使う」戦略のことです。

ここでは、
非同期処理に特化して「どうモックを設計するか」を、
初心者向けにかみ砕いて話していきます。


まずは「何をモックするのか」をはっきりさせる

非同期でよくモック対象になるもの

非同期処理でモックしたくなるものは、だいたい決まっています。

ネットワーク(fetch, axios など)
時間(setTimeout, setInterval, Date.now)
ストレージ(localStorage, IndexedDB)
外部サービス SDK(Firebase, Stripe など)

ポイントは、
「テストを遅く・不安定にするもの」
「テスト環境に存在しないもの」
をモックの候補として見ることです。

そしてもう一つ大事なのが、
「モックしやすいようにコードを設計する」
という発想です。

モックは「テストのための裏技」ではなく、
設計の一部として最初から考えた方が、結果的に楽になります。


戦略1:依存を「引数として渡す」モック化

fetch をモックしやすい形にする

まずは一番シンプルで強力な戦略から。
「外部依存を引数として受け取る」スタイルです。

悪い例から見てみます。

async function fetchCurrentUser() {
  const res = await fetch("/api/user");
  if (!res.ok) {
    throw new Error("ユーザー取得に失敗しました");
  }
  return res.json();
}
JavaScript

この関数をテストしようとすると、
グローバルの fetch を上書きしたり、テストフレームワークの機能に頼ったりする必要が出てきます。

これを「モックしやすい形」に変えると、こうなります。

async function fetchCurrentUser(fetchImpl) {
  const res = await fetchImpl("/api/user");
  if (!res.ok) {
    throw new Error("ユーザー取得に失敗しました");
  }
  return res.json();
}
JavaScript

本番コードではこう呼びます。

const user = await fetchCurrentUser(fetch);
JavaScript

テストでは、fetchImpl にモックを渡せます。

async function fakeFetch() {
  return {
    ok: true,
    json: async () => ({ id: 1, name: "テスト太郎" }),
  };
}

const user = await fetchCurrentUser(fakeFetch);
// user.id === 1, user.name === "テスト太郎" をテストできる
JavaScript

ここでの重要ポイントは、
「モックしやすさは、テストコード側ではなく“プロダクションコードの引数設計”で決まる」
ということです。

「あとからモックしたい」ではなく、
「最初からモックできる形にしておく」が正解に近いです。


戦略2:「インターフェース」を決めてモックを差し替える

fetch 以外にも応用できる形にする

さっきの fetchImpl は、実は「インターフェース」を決めているとも言えます。

「URL を受け取って、{ ok, json } を返す関数」

という約束を作っているわけです。

この「インターフェース」を意識すると、
他の非同期依存にも同じパターンを使えます。

例えば、ユーザーリポジトリを考えます。

async function getUserById(userRepository, id) {
  return userRepository.findById(id);
}
JavaScript

本番では、こういう実装を渡します。

const userRepository = {
  async findById(id) {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error("ユーザー取得に失敗しました");
    return res.json();
  },
};

const user = await getUserById(userRepository, 1);
JavaScript

テストでは、こういうモックを渡します。

const fakeUserRepository = {
  async findById(id) {
    return { id, name: "モックユーザー" };
  },
};

const user = await getUserById(fakeUserRepository, 1);
// user.name が "モックユーザー" であることをテスト
JavaScript

ここでの戦略は、
「非同期依存を“オブジェクトのメソッド”としてまとめ、そのインターフェースをモックで再現する」
というものです。

関数一個ではなく、「役割ごとのオブジェクト」を渡すと、
テスト時に差し替えやすくなります。


戦略3:時間をモックする(タイマー・遅延のテスト)

setTimeout を直接使わない形にする

時間に依存する非同期処理も、モック化戦略がないとテストがつらくなります。

例えば、こういうコード。

function showMessageWithDelay(message) {
  setTimeout(() => {
    console.log(message);
  }, 1000);
}
JavaScript

これをテストしようとすると、
「1秒待つ」か、「テストフレームワークのタイマー機能に依存する」必要が出てきます。

ここでも同じ発想で、
「時間の仕組み」を注入できるようにします。

function showMessageWithDelay(message, { setTimeoutImpl }) {
  setTimeoutImpl(() => {
    console.log(message);
  }, 1000);
}
JavaScript

本番ではこう呼びます。

showMessageWithDelay("hello", { setTimeoutImpl: setTimeout });
JavaScript

テストでは、こう書けます。

const calls = [];

function fakeSetTimeout(fn, ms) {
  calls.push({ fn, ms });
}

showMessageWithDelay("hello", { setTimeoutImpl: fakeSetTimeout });

// calls[0].ms が 1000 かどうかをテスト
// calls[0].fn() を自分で呼んで、console.log の結果を確認する
JavaScript

ここでの重要ポイントは、
「時間に依存する非同期処理も、“時間 API” をモック可能な形にしておく」
ということです。

setTimeout を直接呼ぶのではなく、
「setTimeout 的なもの」を引数で受け取る設計にしておくと、
テストが一気に楽になります。


戦略4:戻り値を「テストしやすい形」にする

副作用だけの非同期関数はモック地獄になりやすい

例えば、こういう関数。

async function saveSettings(settings) {
  const res = await fetch("/api/settings", {
    method: "POST",
    body: JSON.stringify(settings),
  });

  if (!res.ok) {
    alert("保存に失敗しました");
    return;
  }

  alert("保存しました");
}
JavaScript

これをテストしようとすると、

fetch をモックする
alert をモックする

など、モックだらけになります。

ここでの戦略は、
「非同期関数に“意味のある戻り値”を持たせる」
ことです。

例えば、こう変えます。

async function postSettings(fetchImpl, settings) {
  const res = await fetchImpl("/api/settings", {
    method: "POST",
    body: JSON.stringify(settings),
  });

  if (!res.ok) {
    return { ok: false, error: new Error("保存に失敗しました") };
  }

  return { ok: true };
}
JavaScript

テストでは、こう書けます。

async function fakeFetchOk() {
  return { ok: true };
}

async function fakeFetchFail() {
  return { ok: false };
}

const successResult = await postSettings(fakeFetchOk, { theme: "dark" });
// successResult.ok === true

const failResult = await postSettings(fakeFetchFail, { theme: "dark" });
// failResult.ok === false, failResult.error.message をテスト
JavaScript

UI 側では、こう使います。

async function saveSettingsWithUI(settings) {
  const result = await postSettings(fetch, settings);
  if (!result.ok) {
    alert(result.error.message);
  } else {
    alert("保存しました");
  }
}
JavaScript

ここでのポイントは、
「モックする対象を減らすために、“副作用を外に追い出し、非同期関数は結果だけ返すようにする”」
という発想です。


戦略5:モックの「粒度」を意識する

どこまでモックするかを決める

モック化戦略でよくハマるのが、
「モックしすぎて何をテストしているのか分からなくなる」状態です。

例えば、こんな構成を考えます。

fetch をモック
その上のリポジトリをモック
さらにその上のサービスをモック

こうなると、
「結局、本物はどこにもいない」状態になり、
テストの意味が薄くなります。

ここで大事なのは、
「どのレイヤーを“本物”としてテストし、どのレイヤーをモックにするか」
を意識して決めることです。

例えば、

リポジトリのテスト → fetch をモックして、リポジトリのロジックを本物でテスト
サービスのテスト → リポジトリをモックして、サービスのロジックを本物でテスト
UI のテスト → サービスをモックして、UI の振る舞いをテスト

というように、
「一段下のレイヤーだけをモックする」
というルールを決めると、テストの意味がはっきりします。


戦略6:モックを「使い捨て」ではなく「設計の一部」として考える

モックも「API の一種」として設計する

雑にモックを書くと、こうなりがちです。

const fakeFetch = jest.fn().mockResolvedValue({
  ok: true,
  json: async () => ({ id: 1 }),
});
JavaScript

これでも動きますが、
テストファイルごとにバラバラなモックが増えていきます。

もう一歩進めて、
「モックも一つの実装」として設計してしまうのもアリです。

function createFakeFetchUser({ shouldFail = false } = {}) {
  return async function fakeFetch(url) {
    if (!url.startsWith("/api/user")) {
      throw new Error("想定外の URL");
    }

    if (shouldFail) {
      return { ok: false };
    }

    return {
      ok: true,
      json: async () => ({ id: 1, name: "モックユーザー" }),
    };
  };
}
JavaScript

テストでは、こう使えます。

const fetchOk = createFakeFetchUser();
const fetchFail = createFakeFetchUser({ shouldFail: true });
JavaScript

ここでのポイントは、
「モックも“ちゃんとしたコード”として設計しておくと、再利用できるし、テストの意図も読みやすくなる」
ということです。

モックを「その場しのぎのニセモノ」ではなく、
「テスト用のもう一つの実装」として扱うイメージです。


初心者として「モック化戦略」で本当に意識してほしいこと

最後に、あなたの頭の中に置いておいてほしい問いをまとめます。

この非同期関数は、何と何に依存しているか?(fetch, DOM, 時間など)
その依存を「引数」や「オブジェクト」として外から渡せる形にできないか?
テストでは、どのレイヤーを本物としてテストし、どのレイヤーをモックにするか決めているか?
非同期関数は、副作用だけでなく「テストしやすい戻り値」を返すようにできないか?
モックをその場しのぎではなく、「再利用できる小さな実装」として設計できないか?

おすすめの練習は、
自分の非同期関数を一つ選んで、

まず「本物の依存」を全部列挙する
それを一つずつ「引数として渡す形」に書き換えてみる
簡単なモック実装を自分で書いて、テストから呼んでみる

という流れをやってみることです。

一度それができるようになると、
あなたの頭の中に「モック前提で設計するスイッチ」が入ります。

そのスイッチが入ったとき、
あなたはもう「非同期をなんとなくテストする人」ではなく、
「テストまで見据えて非同期を設計できるエンジニア」 にかなり近づいています。「モック化戦略」ってそもそも何の話?

非同期処理をテストしようとすると、必ずぶつかる壁があります。
それが「外部とのやり取り」です。

サーバーへのリクエスト
データベース
タイマー(setTimeout)
ブラウザの API(localStorage, IndexedDB など)

これらを本物のままテストで使うと、

テストが遅い
テストが不安定(ネットワーク次第)
再現性が低い(たまたま通る・たまたま落ちる)

という地獄になります。

そこで出てくるのが モック(mock) です。
「本物の代わりに、テスト用のニセモノを使う」戦略のことです。

ここでは、
非同期処理に特化して「どうモックを設計するか」を、
初心者向けにかみ砕いて話していきます。


まずは「何をモックするのか」をはっきりさせる

非同期でよくモック対象になるもの

非同期処理でモックしたくなるものは、だいたい決まっています。

ネットワーク(fetch, axios など)
時間(setTimeout, setInterval, Date.now)
ストレージ(localStorage, IndexedDB)
外部サービス SDK(Firebase, Stripe など)

ポイントは、
「テストを遅く・不安定にするもの」
「テスト環境に存在しないもの」
をモックの候補として見ることです。

そしてもう一つ大事なのが、
「モックしやすいようにコードを設計する」
という発想です。

モックは「テストのための裏技」ではなく、
設計の一部として最初から考えた方が、結果的に楽になります。


戦略1:依存を「引数として渡す」モック化

fetch をモックしやすい形にする

まずは一番シンプルで強力な戦略から。
「外部依存を引数として受け取る」スタイルです。

悪い例から見てみます。

async function fetchCurrentUser() {
  const res = await fetch("/api/user");
  if (!res.ok) {
    throw new Error("ユーザー取得に失敗しました");
  }
  return res.json();
}
JavaScript

この関数をテストしようとすると、
グローバルの fetch を上書きしたり、テストフレームワークの機能に頼ったりする必要が出てきます。

これを「モックしやすい形」に変えると、こうなります。

async function fetchCurrentUser(fetchImpl) {
  const res = await fetchImpl("/api/user");
  if (!res.ok) {
    throw new Error("ユーザー取得に失敗しました");
  }
  return res.json();
}
JavaScript

本番コードではこう呼びます。

const user = await fetchCurrentUser(fetch);
JavaScript

テストでは、fetchImpl にモックを渡せます。

async function fakeFetch() {
  return {
    ok: true,
    json: async () => ({ id: 1, name: "テスト太郎" }),
  };
}

const user = await fetchCurrentUser(fakeFetch);
// user.id === 1, user.name === "テスト太郎" をテストできる
JavaScript

ここでの重要ポイントは、
「モックしやすさは、テストコード側ではなく“プロダクションコードの引数設計”で決まる」
ということです。

「あとからモックしたい」ではなく、
「最初からモックできる形にしておく」が正解に近いです。


戦略2:「インターフェース」を決めてモックを差し替える

fetch 以外にも応用できる形にする

さっきの fetchImpl は、実は「インターフェース」を決めているとも言えます。

「URL を受け取って、{ ok, json } を返す関数」

という約束を作っているわけです。

この「インターフェース」を意識すると、
他の非同期依存にも同じパターンを使えます。

例えば、ユーザーリポジトリを考えます。

async function getUserById(userRepository, id) {
  return userRepository.findById(id);
}
JavaScript

本番では、こういう実装を渡します。

const userRepository = {
  async findById(id) {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error("ユーザー取得に失敗しました");
    return res.json();
  },
};

const user = await getUserById(userRepository, 1);
JavaScript

テストでは、こういうモックを渡します。

const fakeUserRepository = {
  async findById(id) {
    return { id, name: "モックユーザー" };
  },
};

const user = await getUserById(fakeUserRepository, 1);
// user.name が "モックユーザー" であることをテスト
JavaScript

ここでの戦略は、
「非同期依存を“オブジェクトのメソッド”としてまとめ、そのインターフェースをモックで再現する」
というものです。

関数一個ではなく、「役割ごとのオブジェクト」を渡すと、
テスト時に差し替えやすくなります。


戦略3:時間をモックする(タイマー・遅延のテスト)

setTimeout を直接使わない形にする

時間に依存する非同期処理も、モック化戦略がないとテストがつらくなります。

例えば、こういうコード。

function showMessageWithDelay(message) {
  setTimeout(() => {
    console.log(message);
  }, 1000);
}
JavaScript

これをテストしようとすると、
「1秒待つ」か、「テストフレームワークのタイマー機能に依存する」必要が出てきます。

ここでも同じ発想で、
「時間の仕組み」を注入できるようにします。

function showMessageWithDelay(message, { setTimeoutImpl }) {
  setTimeoutImpl(() => {
    console.log(message);
  }, 1000);
}
JavaScript

本番ではこう呼びます。

showMessageWithDelay("hello", { setTimeoutImpl: setTimeout });
JavaScript

テストでは、こう書けます。

const calls = [];

function fakeSetTimeout(fn, ms) {
  calls.push({ fn, ms });
}

showMessageWithDelay("hello", { setTimeoutImpl: fakeSetTimeout });

// calls[0].ms が 1000 かどうかをテスト
// calls[0].fn() を自分で呼んで、console.log の結果を確認する
JavaScript

ここでの重要ポイントは、
「時間に依存する非同期処理も、“時間 API” をモック可能な形にしておく」
ということです。

setTimeout を直接呼ぶのではなく、
「setTimeout 的なもの」を引数で受け取る設計にしておくと、
テストが一気に楽になります。


戦略4:戻り値を「テストしやすい形」にする

副作用だけの非同期関数はモック地獄になりやすい

例えば、こういう関数。

async function saveSettings(settings) {
  const res = await fetch("/api/settings", {
    method: "POST",
    body: JSON.stringify(settings),
  });

  if (!res.ok) {
    alert("保存に失敗しました");
    return;
  }

  alert("保存しました");
}
JavaScript

これをテストしようとすると、

fetch をモックする
alert をモックする

など、モックだらけになります。

ここでの戦略は、
「非同期関数に“意味のある戻り値”を持たせる」
ことです。

例えば、こう変えます。

async function postSettings(fetchImpl, settings) {
  const res = await fetchImpl("/api/settings", {
    method: "POST",
    body: JSON.stringify(settings),
  });

  if (!res.ok) {
    return { ok: false, error: new Error("保存に失敗しました") };
  }

  return { ok: true };
}
JavaScript

テストでは、こう書けます。

async function fakeFetchOk() {
  return { ok: true };
}

async function fakeFetchFail() {
  return { ok: false };
}

const successResult = await postSettings(fakeFetchOk, { theme: "dark" });
// successResult.ok === true

const failResult = await postSettings(fakeFetchFail, { theme: "dark" });
// failResult.ok === false, failResult.error.message をテスト
JavaScript

UI 側では、こう使います。

async function saveSettingsWithUI(settings) {
  const result = await postSettings(fetch, settings);
  if (!result.ok) {
    alert(result.error.message);
  } else {
    alert("保存しました");
  }
}
JavaScript

ここでのポイントは、
「モックする対象を減らすために、“副作用を外に追い出し、非同期関数は結果だけ返すようにする”」
という発想です。


戦略5:モックの「粒度」を意識する

どこまでモックするかを決める

モック化戦略でよくハマるのが、
「モックしすぎて何をテストしているのか分からなくなる」状態です。

例えば、こんな構成を考えます。

fetch をモック
その上のリポジトリをモック
さらにその上のサービスをモック

こうなると、
「結局、本物はどこにもいない」状態になり、
テストの意味が薄くなります。

ここで大事なのは、
「どのレイヤーを“本物”としてテストし、どのレイヤーをモックにするか」
を意識して決めることです。

例えば、

リポジトリのテスト → fetch をモックして、リポジトリのロジックを本物でテスト
サービスのテスト → リポジトリをモックして、サービスのロジックを本物でテスト
UI のテスト → サービスをモックして、UI の振る舞いをテスト

というように、
「一段下のレイヤーだけをモックする」
というルールを決めると、テストの意味がはっきりします。


戦略6:モックを「使い捨て」ではなく「設計の一部」として考える

モックも「API の一種」として設計する

雑にモックを書くと、こうなりがちです。

const fakeFetch = jest.fn().mockResolvedValue({
  ok: true,
  json: async () => ({ id: 1 }),
});
JavaScript

これでも動きますが、
テストファイルごとにバラバラなモックが増えていきます。

もう一歩進めて、
「モックも一つの実装」として設計してしまうのもアリです。

function createFakeFetchUser({ shouldFail = false } = {}) {
  return async function fakeFetch(url) {
    if (!url.startsWith("/api/user")) {
      throw new Error("想定外の URL");
    }

    if (shouldFail) {
      return { ok: false };
    }

    return {
      ok: true,
      json: async () => ({ id: 1, name: "モックユーザー" }),
    };
  };
}
JavaScript

テストでは、こう使えます。

const fetchOk = createFakeFetchUser();
const fetchFail = createFakeFetchUser({ shouldFail: true });
JavaScript

ここでのポイントは、
「モックも“ちゃんとしたコード”として設計しておくと、再利用できるし、テストの意図も読みやすくなる」
ということです。

モックを「その場しのぎのニセモノ」ではなく、
「テスト用のもう一つの実装」として扱うイメージです。


初心者として「モック化戦略」で本当に意識してほしいこと

最後に、あなたの頭の中に置いておいてほしい問いをまとめます。

この非同期関数は、何と何に依存しているか?(fetch, DOM, 時間など)
その依存を「引数」や「オブジェクト」として外から渡せる形にできないか?
テストでは、どのレイヤーを本物としてテストし、どのレイヤーをモックにするか決めているか?
非同期関数は、副作用だけでなく「テストしやすい戻り値」を返すようにできないか?
モックをその場しのぎではなく、「再利用できる小さな実装」として設計できないか?

おすすめの練習は、
自分の非同期関数を一つ選んで、

まず「本物の依存」を全部列挙する
それを一つずつ「引数として渡す形」に書き換えてみる
簡単なモック実装を自分で書いて、テストから呼んでみる

という流れをやってみることです。

一度それができるようになると、
あなたの頭の中に「モック前提で設計するスイッチ」が入ります。

そのスイッチが入ったとき、
あなたはもう「非同期をなんとなくテストする人」ではなく、
「テストまで見据えて非同期を設計できるエンジニア」 にかなり近づいています。

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