Skip to content

Mocking Browser Requests

The global fetch() function is available in many JavaScript runtimes, including server-side runtimes and web browsers. While fetch() works similarly in both environments, there are some important differences to take into account when testing code that uses fetch() in a browser environment.

Relative URLs with fetch()

Perhaps the most significant difference between using fetch() in a browser as opposed to a server environment is how relative URLs are handled. When you use fetch() in a browser, relative URLs are resolved relative to the current page’s URL. This means that if you call fetch("/api/data") from a page at https://example.com/page, the request will be made to https://example.com/api/data. This happens automatically in the browser whenever you use fetch(), ensuring that requests go to the correct server.

If you try to use a relative URL with fetch() in a server-side environment, you’ll get an error. This is because the server doesn’t know what the base URL should be, so it can’t resolve the relative URL. That means the same fetch() call that works in the browser won’t work in a server environment and it’s important to keep that in mind when writing tests.

Mocking Browser Requests with Mentoss

Mentoss provides a way to mock browser requests in your tests, allowing you to test code that uses fetch() without making actual network requests. To mock a browser request, you can provide a baseUrl option to the FetchMocker constructor. This option specifies the base URL that relative URLs should be resolved against. You can then call a mocked fetch() using a relative URL. Here’s an example:

import { MockServer, FetchMocker } from "mentoss";
import { expect } from "chai";
const BASE_URL = "https://api.example.com";
describe("My API", () => {
const server = new MockServer(BASE_URL);
const mocker = new FetchMocker({
servers: [server],
baseUrl: BASE_URL,
});
// extract the fetch function
const myFetch = mocker.fetch;
// reset the mocker after each test
afterEach(() => {
mocker.clearAll();
});
it("should return a 200 status code", async () => {
// set up the route to test
server.get("/ping", 200);
// make the request
const response = await myFetch("/ping");
// check the response
expect(response.status).to.equal(200);
});
});

In this example, the baseUrl option is set to "https://api.example.com", which means that relative URLs will be resolved against that base URL. The test then calls myFetch("/ping"), which resolves to "https://api.example.com/ping" and makes a request to the server. This allows you to test code that uses relative URLs with fetch() in a browser-like environment.

Mocking CORS Requests

Another important consideration when mocking browser requests is how to handle CORS (Cross-Origin Resource Sharing) requests. CORS is a security feature that restricts which domains can make requests to a server. When you make a request from one domain to another, the server must include the appropriate CORS headers to allow the request to go through. When you set the baseUrl option in the FetchMocker constructor, Mentoss automatically includes the appropriate CORS headers when a request is made to a different origin.

Simple Requests

For simple requests, Mentoss automatically includes the Origin header on the request. If the server responds with the appropriate Access-Control-Allow-Origin header, your code will receive the response as expected. If the server does not respond with the appropriate header, the request will fail. Here’s an example:

import { MockServer, FetchMocker } from "mentoss";
const BASE_URL = "https://api.example.com";
describe("My API", () => {
const server = new MockServer("https://www.example.com");
const mocker = new FetchMocker({
servers: [server],
baseUrl: BASE_URL,
});
// extract the fetch function
const myFetch = mocker.fetch;
it("should return a 200 status code", async () => {
// set up the route to test
server.get("/ping", {
status: 200,
headers: {
"Access-Control-Allow-Origin": BASE_URL,
},
body: { message: "pong" },
});
// make the request
const response = await myFetch("https://www.example.com/ping");
// check the response
expect(response.status).to.equal(200);
});
});

In this example, the server responds with the Access-Control-Allow-Origin header set to the base URL "https://api.example.com". This allows the request to go through because the origin matches the origin of the fetch mocker’s baseUrl, and the test passes.

Preflighted Requests

For preflighted requests, Mentoss automatically sends an OPTIONS request to the server to check if the request is allowed. If the server responds with the appropriate Access-Control-Allow-Methods and Access-Control-Allow-Headers headers, the original request will go through. If the server does not respond with the appropriate headers, the request will fail. Here’s an example:

import { MockServer, FetchMocker } from "mentoss";
const BASE_URL = "https://api.example.com";
describe("My API", () => {
const server = new MockServer("https://www.example.com");
const mocker = new FetchMocker({
servers: [server],
baseUrl: BASE_URL,
});
// extract the fetch function
const myFetch = mocker.fetch;
it("should return a 200 status code", async () => {
// this is for the preflight request
server.options("/ping", {
status: 200,
headers: {
"Access-Control-Allow-Origin": BASE_URL,
"Access-Control-Allow-Headers": "Content-Type",
},
});
// this is the route that we want to call
server.get("/ping", {
status: 200,
headers: {
"Access-Control-Allow-Origin": BASE_URL,
},
body: { message: "pong" },
});
// make the request
const response = await myFetch("https://www.example.com/ping", {
headers: {
"Content-Type": "application/json",
},
});
// check the response
expect(response.status).to.equal(200);
});
});

In this example, the code attempts a request to "https://www.example.com/ping". Because this is a cross-origin request, Mentoss first sends an OPTIONS request to the server to check if the request is allowed. The server responds to the preflight request with the appropriate Access-Control-Allow-Origin and Access-Control-Allow-Headers headers, allowing the original request to go through.

The Preflight Cache

When a browser makes a preflight request, it caches the response for a period of time (usually 5 minutes but customizable using the Access-Control-Max-Age header). This means that if you make the same request again within the cache period, the browser won’t send another preflight request. Instead, it will use the cached response to determine if the request is allowed. Mentoss does not currently support caching preflight responses, so each preflight request will result in a new preflight request to the server.

Mentoss caches preflight responses in memory for the duration of the test run. This means that if you make the same preflighted request multiple times within a single test, Mentoss will only send the OPTIONS request once. However, you can manually clear the preflight cache by calling the clearPreflightCache() method. It’s helpful to do this after each test

import { MockServer, FetchMocker } from "mentoss";
const BASE_URL = "https://api.example.com";
describe("My API", () => {
const server = new MockServer("https://www.example.com");
const mocker = new FetchMocker({
servers: [server],
baseUrl: BASE_URL,
});
// extract the fetch function
const myFetch = mocker.fetch;
// clear preflight cache entries
afterEach(() => {
mocker.clearPreflightCache();
});
it("should return a 200 status code", async () => {
// this is for the preflight request
server.options("/ping", {
status: 200,
headers: {
"Access-Control-Allow-Origin": BASE_URL,
"Access-Control-Allow-Headers": "Content-Type",
},
});
// this is the route that we want to call
server.get("/ping", {
status: 200,
headers: {
"Access-Control-Allow-Origin": BASE_URL,
},
body: { message: "pong" },
});
// make the request
const response = await myFetch("https://www.example.com/ping", {
headers: {
"Content-Type": "application/json",
},
});
// check the response
expect(response.status).to.equal(200);
});
});