Skip to main content

Mocky Balboa and Next.js

How to use Mocky Balboa with Next.js.

Supported versions

≥ 14.0.0

Installation

pnpm add -D @mocky-balboa/next-js

Usage

There are two ways to use Mocky Balboa with Next.js. Using the CLI or programmatically. The CLI is the recommended way to use Mocky Balboa with Next.js as it's easier to get started with.

Using the CLI

pnpm mocky-balboa-next-js

The CLI tool will source your Next.js configuration file when you don't explicitly specify it with the --conf flag. Starting from the current working directory and traversing up until it finds a Next.js configuration file, it will look for the following files:

  • next.config.js
  • next.config.mjs
  • next.config.ts
  • next.config.cjs

CLI options

Usage: mocky-balboa-next-js [options]

Starts the Next.js server for your application as well as the necessary mocky-balboa servers

Options:
-p, --port [port] Port to run the server on (default: "3000")
--websocket-port [websocketPort] Port to run the WebSocket server on (default: "58152")
-h, --hostname [hostname] Hostname to bind the server to (default: "0.0.0.0")
-t, --timeout [timeout] Timeout in milliseconds for the mock server to receive a response from the client (default: "5000")
--https Enable https server. Either https or http server is run, not both. When no --https-cert and --https-key are
provided, a self-signed certificate will be automatically generated.
--https-cert [certPath] Optional path to the https certificate file
--https-ca [caPath] Optional path to the https Certificate Authority file
--https-key [keyPath] Optional path to the https key file
-d, --dev Run the Next.js server in development mode (default: false)
-q, --quiet Hide error messages containing server information (default: false)
--conf [conf] Relative or absolute path to the Next.js configuration file. Include the file extension. Defaults to
discovering the path traversing up from the current working directory.
--help display help for command

Programmatically

You might prefer to use the startServers function programmatically, especially if you have other customizations or configurations that you want to apply on the same server process.

import { startServers } from "@mocky-balboa/next-js";
import next from "next";
import nextConfig from "../path/to/next.config.js";

const main = async () => {
await startServers(
(options) => {
return next({
...options,
// If you want to enable experimental features that are available to enable via the options
// experimentalHttpsServer: true,
// experimentalTestProxy: true,
conf: nextConfig,
});
},
);
};

void main();

See startServers API reference for full documentation on how to use startServers.

Working with Next.js caching

When using the app directory in Next.js unless you opt out of caching in your application logic, then the server will cache fetch requests and app routes. This can become problematic in a test suite when determinism is required for robust testing.

If you're application uses the dynamic = 'force-dynamic' or revalidate = 0 directive, then you have already opted out of route level caching. However, you'll still encounter cached fetch requests (GET) unless you also opt out of fetch request caching.

Don't worry, you're not going to have to start exporting conditional directives in your application to facilitate the tests. The whole ethos of Mocky Balboa is to provide a solution without having to modify your application logic.

Fetch (data) caching

The easiest way to opt out of fetch request caching is to use the custom CacheHandler in your Next.js configuration from @mocky-balboa/next-js. This cache handler acts as a no-op cache handler that will disable the persistent cache.

ESM example

// next.config.ts
import type { NextConfig } from "next";
import { createRequire } from "node:module";

const require = createRequire(import.meta.url);

const nextConfig: NextConfig = {
cacheHandler: require.resolve("@mocky-balboa/next-js/cache-handler"),
cacheMaxMemorySize: 0,
};

export default nextConfig;

CJS example

// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
cacheHandler: require.resolve("@mocky-balboa/next-js/cache-handler"),
cacheMaxMemorySize: 0,
};

export default nextConfig;

App router caching

tip

This section of documentation is only applicable to pages in the app router that opt-in to full dynamic caching and you want to run your tests in parallel against the same server instance.

Using the CacheHandler as shown in the example above will opt out of the persistent cache for the app router. However concurrent requests to the same route will result in the route only being rendered once. Each incoming request will share the same stream from the renderer. This in turn means that testing the same route in parallel could cause issues.

There are two ways to get around this behaviour:

  1. Run your tests serially. The obvious drawback here is that your tests will take longer to run.
  2. Spin up a new server instance for each of these tests. Check out the example below on how to do this in Playwright.
import { test, expect } from "@playwright/test";
import { createClient, type Client } from "@mocky-balboa/playwright";
import getPort from "get-port";
import { detect } from "detect-port";
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";

// Waits for a port to be occupied checking once every second for
// a maximum of 10 seconds.
const waitForPortToBeOccupied = async (port: number) => {
const waitFor = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms));
let ticks = 0;
while (ticks < 10) {
await waitFor(1000);
const realPort = await detect(port);
if (realPort !== port) {
return;
}
ticks++;
}

throw new Error(`Timed out waiting for port ${port} to be occupied`);
};

let client: Client;
let applicationPort: number;
let serverProcess: ChildProcessWithoutNullStreams;
test.beforeEach(async ({ context }) => {
// Find a free port for the application
applicationPort = await getPort();
// Find a free port for the websocket server
const websocketServerPort = await getPort();
console.log(
`Starting server on port ${applicationPort} and websocket port ${websocketServerPort}`,
);

// Start the Next.js server via Mocky Balboa
serverProcess = spawn("pnpm", [
"mocky-balboa-next-js",
"--port",
applicationPort.toString(),
"--websocket-port",
websocketServerPort.toString(),
]);

// Log all stdout from the server process
serverProcess.stdout.on("data", (data) => {
console.log(data.toString());
});

// Log all stderr from the server process
serverProcess.stderr.on("data", (data) => {
console.error(data.toString());
});

// Wait for the server processes to be ready
await Promise.all([
waitForPortToBeOccupied(applicationPort),
waitForPortToBeOccupied(websocketServerPort),
]);

// Create the Mocky Balboa Playwright client to run against
// the server we just started
client = await createClient(context, {
port: websocketServerPort,
});
});

test.afterEach(async () => {
// Kill the server process after each test
serverProcess.kill();
});

test("...", async ({
page,
}) => {
// Visit pages on the server we started
await page.goto(`http://localhost:${applicationPort}`);
});

The obvious drawback here being that we need a new server process for each test. This can be inefficient and slow down our tests when used in excess, but can be a good strategy when you need to test different configurations or scenarios (in parallel) for routes which opt-in to full dynamic caching.

Further reading

If you want to learn more about caching in Next.js, I'd recommend checking out the official documentation.