Skip to content

Commit 9e7efe4

Browse files
Copilotnzakas
andauthored
feat: Add MockAgent class for undici Dispatcher support (#119)
* Initial plan * Add MockAgent class with comprehensive tests Co-authored-by: nzakas <38546+nzakas@users.noreply.github.com> * Fix TypeScript errors by adding mock-agent.d.ts type definitions Co-authored-by: nzakas <38546+nzakas@users.noreply.github.com> * Add comprehensive documentation for MockAgent Co-authored-by: nzakas <38546+nzakas@users.noreply.github.com> * Document limitations in MockAgent dispatch implementation Co-authored-by: nzakas <38546+nzakas@users.noreply.github.com> * docs: Update MockAgent documentation per review feedback - Add backticks around 'Dispatcher' in first sentence - Mention Node.js fetch() support in "What is undici?" section - Add new section for Node.js fetch() usage before undici section - Clarify MockAgent is for undici/Node.js fetch, not browser fetch Co-authored-by: nzakas <38546+nzakas@users.noreply.github.com> * refactor: Move type definitions from .d.ts to JSDoc in mock-agent.js - Remove src/mock-agent.d.ts from source control per review feedback - Enhance JSDoc type definitions with proper function signatures - Add @ts-nocheck to suppress Buffer type errors - Type definitions now follow pattern of mock-server.js Co-authored-by: nzakas <38546+nzakas@users.noreply.github.com> * docs: Add warning about Node.js 18+ requirement for MockAgent - Add Aside component warning that MockAgent is for Node.js 18+ only - Clarify it does not work in browsers - Direct users to FetchMocker for browser testing Co-authored-by: nzakas <38546+nzakas@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: nzakas <38546+nzakas@users.noreply.github.com>
1 parent b817adc commit 9e7efe4

6 files changed

Lines changed: 1284 additions & 2 deletions

File tree

README.md

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@ npm install mentoss
2020

2121
## Usage
2222

23-
There are two primary classes in Mentoss:
23+
There are three primary classes in Mentoss:
2424

2525
1. `MockServer` - a server implementation where you can mock out requests and responses
26-
1. `FetchMocker` - the utility that creates a new `fetch()` function that calls one or more `MockServers`
26+
2. `FetchMocker` - the utility that creates a new `fetch()` function that calls one or more `MockServers`
27+
3. `MockAgent` - an undici Dispatcher that intercepts undici requests and routes them to `MockServers`
28+
29+
### Using with `fetch()` (browser and Node.js)
2730

2831
In general, you'll create a `MockServer` first and then create a `FetchMocker`, like this:
2932

@@ -86,6 +89,42 @@ server.clear();
8689
mocker.clearAll();
8790
```
8891

92+
### Using with undici (Node.js only)
93+
94+
If you're using [undici](https://undici.nodejs.org/) for HTTP requests, you can use `MockAgent` as a dispatcher:
95+
96+
```js
97+
import { MockServer, MockAgent } from "mentoss";
98+
import { request } from "undici";
99+
100+
// create a new server with the given base URL
101+
const server = new MockServer("https://api.example.com");
102+
103+
// simple mocked route
104+
server.get("/foo/bar", { status: 200, body: "OK" });
105+
106+
// create an agent that uses the server
107+
const agent = new MockAgent({
108+
servers: [server],
109+
});
110+
111+
// make a request using the agent as a dispatcher
112+
const { statusCode, body } = await request("https://api.example.com/foo/bar", {
113+
dispatcher: agent,
114+
});
115+
116+
// check that the request was made
117+
assert(agent.called("https://api.example.com/foo/bar"));
118+
119+
// check that all routes were called
120+
assert(agent.allRoutesCalled());
121+
122+
// clear the agent
123+
agent.clearAll();
124+
```
125+
126+
Note: `MockAgent` does not support `baseUrl` or `credentials` options, as these are only relevant for browser contexts.
127+
89128
## Development
90129

91130
To work on Mentoss, you'll need:

docs/astro.config.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ export default defineConfig({
2828
label: "Fetch Mockers",
2929
autogenerate: { directory: "fetch-mockers" },
3030
},
31+
{
32+
label: "Mock Agents",
33+
autogenerate: { directory: "mock-agents" },
34+
},
3135
],
3236
components: {
3337
Footer: "./src/components/MyFooter.astro",
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
---
2+
title: Creating a Mock Agent
3+
description: Learn how to create a mock agent for undici
4+
---
5+
6+
import { Aside } from "@astrojs/starlight/components";
7+
8+
A _mock agent_ in Mentoss is an undici `Dispatcher` that intercepts HTTP requests made by undici and returns responses that you've defined in your mock servers. This allows you to test Node.js applications that use undici without making real network requests.
9+
10+
<Aside type="caution">
11+
`MockAgent` is designed for Node.js 18+ only and will not work in browsers. For browser testing, use `FetchMocker` instead.
12+
</Aside>
13+
14+
## What is undici?
15+
16+
[undici](https://undici.nodejs.org/) is a high-performance HTTP client for Node.js. It's used internally by Node.js for `fetch()` and is also available as a standalone library. If you're testing code that uses undici's `request()`, `stream()`, or `pipeline()` methods, or Node.js's native `fetch()` function, you'll want to use `MockAgent`.
17+
18+
## Create a new `MockAgent` instance
19+
20+
To get started, import the `MockAgent` class and create a new instance. The only required argument is an object with the following property:
21+
22+
- `servers` (required) - an array of `MockServer` instances to use for mocking requests
23+
24+
Here's an example:
25+
26+
```js
27+
import { MockServer, MockAgent } from "mentoss";
28+
29+
const server = new MockServer("https://api.example.com");
30+
31+
const agent = new MockAgent({
32+
servers: [server],
33+
});
34+
```
35+
36+
With your mock agent created, you can now use it with Node.js `fetch()` or undici.
37+
38+
## Use the mock agent with Node.js `fetch()`
39+
40+
Node.js has a native `fetch()` function (available in Node.js 18+) that accepts a `dispatcher` option. You can use `MockAgent` as the dispatcher to mock requests:
41+
42+
```js
43+
import { MockServer, MockAgent } from "mentoss";
44+
import { expect } from "chai";
45+
46+
describe("My API with Node.js fetch()", () => {
47+
const server = new MockServer("https://api.example.com");
48+
const agent = new MockAgent({
49+
servers: [server],
50+
});
51+
52+
// reset the server after each test
53+
afterEach(() => {
54+
agent.clearAll();
55+
});
56+
57+
it("should return a 200 status code", async () => {
58+
// set up the route to test
59+
server.get("/ping", { status: 200, body: "pong" });
60+
61+
// make the request using the mock agent
62+
const response = await fetch("https://api.example.com/ping", {
63+
dispatcher: agent,
64+
});
65+
66+
// check the response
67+
expect(response.status).to.equal(200);
68+
69+
// read the body
70+
const bodyText = await response.text();
71+
expect(bodyText).to.equal("pong");
72+
});
73+
});
74+
```
75+
76+
## Use the mock agent with undici
77+
78+
The `MockAgent` class implements the undici `Dispatcher` interface, which means you can use it anywhere undici accepts a dispatcher. Here's an example using undici's `request()` method:
79+
80+
```js
81+
import { MockServer, MockAgent } from "mentoss";
82+
import { request } from "undici";
83+
import { expect } from "chai";
84+
85+
describe("My API with undici", () => {
86+
const server = new MockServer("https://api.example.com");
87+
const agent = new MockAgent({
88+
servers: [server],
89+
});
90+
91+
// reset the server after each test
92+
afterEach(() => {
93+
agent.clearAll();
94+
});
95+
96+
it("should return a 200 status code", async () => {
97+
// set up the route to test
98+
server.get("/ping", { status: 200, body: "pong" });
99+
100+
// make the request using the mock agent
101+
const { statusCode, body } = await request(
102+
"https://api.example.com/ping",
103+
{
104+
dispatcher: agent,
105+
},
106+
);
107+
108+
// check the response
109+
expect(statusCode).to.equal(200);
110+
111+
// read the body
112+
const bodyText = await body.text();
113+
expect(bodyText).to.equal("pong");
114+
});
115+
});
116+
```
117+
118+
## Differences from FetchMocker
119+
120+
The `MockAgent` class is similar to `FetchMocker` but has some key differences:
121+
122+
### What's the same:
123+
- Both use an array of `MockServer` instances to define routes
124+
- Both provide the same testing helpers: `called()`, `allRoutesCalled()`, `uncalledRoutes`, and `assertAllRoutesCalled()`
125+
- Both provide a `clearAll()` method to reset the servers
126+
127+
### What's different:
128+
- `MockAgent` does not support `baseUrl` - undici and Node.js fetch requests always use absolute URLs
129+
- `MockAgent` does not support `credentials` - credentials are only relevant for browser contexts
130+
- `MockAgent` implements the undici `Dispatcher` interface with `dispatch()`, `close()`, and `destroy()` methods
131+
- `MockAgent` is specifically designed for use with undici and Node.js `fetch()`, not browser `fetch()`
132+
133+
## Testing helpers
134+
135+
Like `FetchMocker`, `MockAgent` provides several testing helpers to verify that your requests were made correctly:
136+
137+
```js
138+
const server = new MockServer("https://api.example.com");
139+
server.get("/users", { status: 200, body: [] });
140+
server.post("/users", { status: 201 });
141+
142+
const agent = new MockAgent({ servers: [server] });
143+
144+
// make a request
145+
await request("https://api.example.com/users", { dispatcher: agent });
146+
147+
// check if a specific request was made
148+
agent.called("https://api.example.com/users"); // true
149+
agent.called({ method: "POST", url: "https://api.example.com/users" }); // false
150+
151+
// check if all routes were called
152+
agent.allRoutesCalled(); // false (POST not called)
153+
154+
// get a list of uncalled routes
155+
agent.uncalledRoutes; // ["POST https://api.example.com/users -> 201"]
156+
157+
// throw an error if not all routes were called
158+
agent.assertAllRoutesCalled(); // throws Error
159+
```
160+
161+
## Closing the agent
162+
163+
When you're done with the agent, you can close it to prevent new requests:
164+
165+
```js
166+
await agent.close();
167+
// or
168+
await agent.destroy(); // alias for close()
169+
```
170+
171+
After closing, any new dispatch attempts will fail with an error.

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@
66

77
export { MockServer } from "./mock-server.js";
88
export { FetchMocker } from "./fetch-mocker.js";
9+
export { MockAgent } from "./mock-agent.js";
910
export { CookieCredentials } from "./cookie-credentials.js";
1011
export * from "./types.js";

0 commit comments

Comments
 (0)