Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to mock internal fetch from end-to-end (e2e) test? #609

Closed
sofimiazzi opened this issue Jun 18, 2023 · 4 comments
Closed

How to mock internal fetch from end-to-end (e2e) test? #609

sofimiazzi opened this issue Jun 18, 2023 · 4 comments

Comments

@sofimiazzi
Copy link

Hi, there! I am trying to write an end-to-end (e2e) test for my application, which involves making a request to an external API using the fetch function. However, I want to mock the internal fetch call in order to control the response and ensure predictable testing. Currently, when I run the test, the response does not match the expected values. I would appreciate any assistance in understanding how to mock the internal fetch call correctly in my e2e test.

Repository with the complete example code cfw-mock (just install the packages and run npm run test).


Here is the code snippet for the application:

import { Hono } from "hono";

const app = new Hono();

app.get("/users/:id", async (c) => {
  const userId = c.req.param("id");

  const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
  const user = await response.json();

  return c.json(user);
});

export default app;

And here is the code snippet for the e2e test:

import { join } from "path";
import { UnstableDevWorker, unstable_dev } from "wrangler";

const relativePath = (path: string) => join(__dirname, ...path.split("/"));
let worker: UnstableDevWorker;
let apiUrl: string;

beforeAll(async () => {
  worker = await unstable_dev(relativePath("../src/worker.ts"), { experimental: { disableExperimentalWarning: true } });
  apiUrl = `http://localhost:${worker.port}`;
});

afterAll(async () => {
  await worker.stop();
});

describe("GET /users/:id", () => {
  it("should return a specific user", async () => {
    const expectedUser = { id: 1, name: "John Doe" };

    // mocking internal fetch call
    const fetchMock = getMiniflareFetchMock();
    const origin = fetchMock.get("https://jsonplaceholder.typicode.com");
    origin.intercept({ method: "GET", path: "/users/1" }).reply(200, expectedUser);

    const response = await fetch(`${apiUrl}/users/1`);
    const user = await response.json();

    expect(user).toHaveProperty("name", expectedUser.name); // asserting response fails :(
    // AssertionError: expected { id: 1, name: 'Leanne Graham', …(6) } to have property "name" with value 'John Doe'
  });
});

In my e2e test, I have a specific scenario where I need to mock the internal fetch call to control the response and validate the behavior of my application. I have attempted to use the getMiniflareFetchMock() function to create a mock, and I intercept the GET request to /users/1 and respond with the expected user object. However, when I run the test, the response does not match the expected values, and the assertion fails.

I would appreciate any guidance or suggestions on how to properly mock the internal fetch call in my e2e test, so I can ensure predictable testing and validate the response data.

@shaunco
Copy link

shaunco commented Aug 13, 2023

I'm having the same issue with getMiniflareFetchMock() call (with disableNetConnect() and various interceptors set up) in my vitest beforeAll seemingly having no effect on the separate miniflare 2.14.0 instance that is hosting my worker, as the worker's fetch calls to external resources still hit those external resources instead of the MockAgent.

I'm going to try removing the environment: miniflare setting in vitest and instead go with the Miniflare API:

import { Miniflare } from "miniflare";

const mf = new Miniflare({...});

afterAll(() => {
	mf.dispose();
});

to see if having miniflare in the same process as vitest (and the getMiniflareFetchMock() call) fixes it.

edit: based on #588 (comment) - it would appear the move to workerd in miniflare v3 did break fetch mocking.

edit2: Looks like #632 added the fetchMock option to the Miniflare API I suggested above (using the change in #629). That said, getMiniflareFetchMock() doesn't seem wired up to it yet.

@sofimiazzi
Copy link
Author

sofimiazzi commented Aug 29, 2023

I achieve this goal with the "miniflare" and "@miniflare/core" package in version 2.14.0. I suggest doing something similar to this:

import { createFetchMock } from "@miniflare/core";
import getPort from "get-port";

// use join path
const MAIN_ENTRYPOINT_PATH = "./dist/index.js";
const WRANGLER_CONFIG_PATH = "./wrangler.toml";

const createWorker = async (fetchMock) => {
  const options = {
    host: "localhost",
    port: await getPort(),
    scriptPath: MAIN_ENTRYPOINT_PATH,
    wranglerConfigPath: WRANGLER_CONFIG_PATH,
    modules: true,
    d1Persist: true,
    d1Databases: ["__D1_BETA__DB"],
    fetchMock,
  };
  const url = `http://${options.host}:${options.port}`;
  const worker = new Miniflare(options);
  await worker.startServer();

  return {
    url,
    worker,
    stop: async () => await worker.stop(),
  };
};

let fetchMock: ReturnType<typeof createFetchMock>;
let server: any; // type according to your server

beforeAll(async () => {
  fetchMock = createFetchMock();
  server = await createWorker(fetchMock);
});

afterAll(async () => {
  await server.stop();
});

test("worker", async () => {
  const expectedUser = { id: 1, name: "John Doe" };
  const origin = fetchMock.get("https://jsonplaceholder.typicode.com");
  origin
    .intercept({ method: "GET", path: "/users/1" })
    .reply(200, expectedUser);

  const response = await fetch(`${server.url}/users/1`);
  const user = await response.json();

  expect(user).toHaveProperty("name", expectedUser.name);
});

@mrbbot
Copy link
Contributor

mrbbot commented Nov 7, 2023

Hey! 👋 Apologies for not replying here, but it looks like you've got a solution. I'm going to close this now. 👍

@mrbbot mrbbot closed this as completed Nov 7, 2023
@sterlingwes
Copy link

@sofimiazzi solution doesn't work anymore with miniflare 3 (no export for createFetchMock), can we reopen @mrbbot ? The docs are missing for Testing under miniflare and the fetchMock section on the Workers cloudflare:test module guide seems to indicate that you can only mock the worker under test which seems pretty.. pointless? Unless I'm misunderstanding the example here:

import { fetchMock } from "cloudflare:test";
import { beforeAll, afterEach, it, expect } from "vitest";

beforeAll(() => {
  // Enable outbound request mocking...
  fetchMock.activate();
  // ...and throw errors if an outbound request isn't mocked
  fetchMock.disableNetConnect();
});
// Ensure we matched every mock we defined
afterEach(() => fetchMock.assertNoPendingInterceptors());

it("mocks requests", async () => {
  // Mock the first request to `https://example.com`
  fetchMock
    .get("https://example.com")
    .intercept({ path: "/" })
    .reply(200, "body");

  const response = await fetch("https://example.com/");
  expect(await response.text()).toBe("body");
});

Assuming example.com is the worker under test, why would you mock the worker you're trying to test?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants