diff --git a/tests/utils/fetch-utils.ts b/tests/utils/fetch-utils.ts
new file mode 100644
index 0000000..3fbe08f
--- /dev/null
+++ b/tests/utils/fetch-utils.ts
@@ -0,0 +1,87 @@
+import { readFile } from "node:fs/promises";
+import { existsSync } from "node:fs";
+import { resolve } from "node:path";
+import { Fetch, HttpMethod, HttpRequest, HttpResponse, createFetch } from "@/utils/net";
+
+type FetchInterceptorResponse = string | string[] | HttpResponse | unknown;
+
+type FetchInterceptorHandler = (args: string[], request: HttpRequest, url: string) => FetchInterceptorResponse | Promise<FetchInterceptorResponse>;
+
+type FetchInterceptor = {
+    baseUrl?: string;
+
+    requiredHeaders?: string[];
+} & {
+    [Method in HttpMethod]?: {
+        [url: string]: FetchInterceptorHandler;
+    };
+};
+
+const FAKE_FETCH: Fetch = (url, request) => Promise.reject(
+    new Error(`Unsupported request: '${normalizeHttpMethod(request?.method)} ${url}'`)
+);
+
+function normalizeHttpMethod(method: string): HttpMethod {
+    return (method?.toUpperCase() as HttpMethod) || "GET";
+}
+
+async function normalizeResponse(response: FetchInterceptorResponse): Promise<HttpResponse> {
+    if (typeof response === "string") {
+        return HttpResponse.text(response);
+    }
+
+    if (Array.isArray(response) && response.length && response.every(x => typeof x === "string")) {
+        const path = resolve(...response);
+        if (existsSync(path)) {
+            const content = await readFile(path, "utf8");
+            return HttpResponse.text(content);
+        }
+    }
+
+    if (typeof response["url"] === "string" && typeof response["blob"] === "function") {
+        return response as HttpResponse;
+    }
+
+    return HttpResponse.json(response);
+}
+
+export function createFakeFetch(interceptor: FetchInterceptor): Fetch {
+    const baseUrl = interceptor.baseUrl?.endsWith("/") ? interceptor.baseUrl.slice(0, -1) : (interceptor.baseUrl || "");
+    const requiredHeaders = interceptor.requiredHeaders || [];
+    const methods = interceptor;
+
+    return createFetch({ handler: FAKE_FETCH }).use(async (url, request, next) => {
+        url = String(url);
+        const urlPath = url.startsWith(baseUrl) ? url.substring(baseUrl.length) : url;
+
+        const handlerEntry = Object.entries(methods[normalizeHttpMethod(request?.method)] || {})
+            .map(([urlMatcher, handler]) => [urlPath.match(urlMatcher), handler] as const)
+            .find(([match]) => match !== null);
+
+        if (!handlerEntry || !requiredHeaders.every(header => request?.headers?.[header])) {
+            return await next(url, request);
+        }
+
+        const args = handlerEntry[0].slice(1);
+        const response = await handlerEntry[1](args, request, url);
+        return await normalizeResponse(response);
+    });
+}
+
+export function createCombinedFetch(...fetchComponents: Fetch[]): Fetch {
+    const handlers = [...fetchComponents].reverse();
+
+    return async (url, request) => {
+        for (let i = handlers.length - 1; i >= 0; i--) {
+            try {
+                return await handlers[i](url, request);
+            } catch (error: unknown) {
+                if (i === 0) {
+                    throw error;
+                }
+            }
+        }
+
+        return HttpResponse.error();
+    };
+}