← ./articles-ja

APIがfreezeされてmockできない時はtransport layerをmockする

一部のruntimeは、高レベルAPIをpageへ注入し、アプリcodeが走る前にfreezeします。その場合、test中でもAPIを直接monkey patchできません。

実用的なfallbackは1段下へ降りることです。APIが内部で使うtransportを見つけ、そこをinterceptします。

症状

testやautomation codeがruntime APIをwrapしようとします。

const originalInvoke = window.__APP_INTERNALS__.invoke;

window.__APP_INTERNALS__.invoke = async (command, args) => {
  if (command === "load_projects") {
    return [{ id: "p1", name: "Example" }];
  }

  return originalInvoke(command, args);
};

しかしcallはreal backendへ行く。Object.defineProperty がthrowする。assignmentが成功したように見えても効果がない。

descriptorを確認します。

console.log(
  Object.getOwnPropertyDescriptor(window.__APP_INTERNALS__, "invoke")
);

writable: false かつ configurable: false なら、そのpropertyはfreezeされています。そのlayerでは置き換えられません。

transportを探す

高レベルAPIは、最終的に低レベルtransportを使います。

runtime.invoke(command, args)
  -> fetch(...)

runtime.send(message)
  -> postMessage(...)

client.call(payload)
  -> WebSocket.send(...)

DevTools、source inspection、一時的なnetwork loggerでtransportを確認します。

たとえばIPC風APIが内部URLへ fetch() しているかもしれません。

http://ipc.localhost/load_projects

高レベルAPIがfreezeされていても fetch がwrap可能なら、fetch をinterceptします。

fallthrough付きでfetchをinterceptする

mockは狭くし、それ以外は通します。

const originalFetch = window.fetch.bind(window);

window.fetch = async (input, init) => {
  const url = typeof input === "string" ? input : input.url;

  if (url.startsWith("http://ipc.localhost/load_projects")) {
    return new Response(
      JSON.stringify([{ id: "p1", name: "Example" }]),
      {
        status: 200,
        headers: { "content-type": "application/json" },
      }
    );
  }

  return originalFetch(input, init);
};

fallthroughが重要です。test対象のcommandだけをmockし、無関係なrequestは本来の挙動へ通します。

app初期化前にinstallする

transport interceptionは、app codeが初期化する前に入れる必要があります。browser automationではinit scriptを使います。

await page.addInitScript(() => {
  const originalFetch = window.fetch.bind(window);

  window.fetch = async (input, init) => {
    const url = typeof input === "string" ? input : input.url;

    if (url.startsWith("http://ipc.localhost/load_projects")) {
      return new Response(JSON.stringify([]), {
        status: 200,
        headers: { "content-type": "application/json" },
      });
    }

    return originalFetch(input, init);
  };
});

appが元のtransportを先にcaptureすると、後から置き換えても効かないことがあります。

判断チェック

  1. Object.getOwnPropertyDescriptor で高レベルAPIがfreezeされているか確認する
  2. 実transportを特定する: fetchpostMessage、WebSocket、custom protocol、native bridge
  3. test contextでtransportをwrapできるか確認する
  4. app初期化前にwrapperをinstallする
  5. target route/messageだけをmockする
  6. unknown requestはoriginal transportへ通す
  7. 開発中だけlogし、最終testからnoisy logを消す

限界

transport-layer mockは強力ですが、integration bugを隠すために使うものではありません。

向く場面です。

  • runtimeが高レベルAPI置換を禁止する
  • test対象がfrontend behavior
  • deterministic backend responseが必要
  • CIでreal backendが使えない

real integrationを優先する場面です。

  • transport protocol自体をtestしている
  • auth、permission、security policyが対象
  • request serializationが疑わしい

参考