diff --git a/docs/docs/api/MockAgent.md b/docs/docs/api/MockAgent.md index 70d479ac618..3f5d9bf5575 100644 --- a/docs/docs/api/MockAgent.md +++ b/docs/docs/api/MockAgent.md @@ -179,7 +179,9 @@ for await (const data of result2.body) { console.log('data', data.toString('utf8')) // data hello } ``` + #### Example - Mock different requests within the same file + ```js const { MockAgent, setGlobalDispatcher } = require('undici'); const agent = new MockAgent(); @@ -540,3 +542,125 @@ agent.assertNoPendingInterceptors() // │ 0 │ 'GET' │ 'https://example.com' │ '/' │ 200 │ '❌' │ 0 │ 1 │ // └─────────┴────────┴───────────────────────┴──────┴─────────────┴────────────┴─────────────┴───────────┘ ``` + +#### Example - access call history on MockAgent + +By default, every call made within a MockAgent have their request configuration historied + +```js +import { MockAgent, setGlobalDispatcher, request } from 'undici' + +const mockAgent = new MockAgent() +setGlobalDispatcher(mockAgent) + +await request('http://example.com', { query: { item: 1 }}) + +mockAgent.getCallHistory().firstCall() +// Returns +// MockCallHistoryLog { +// body: undefined, +// headers: undefined, +// method: 'GET', +// origin: 'http://example.com', +// fullUrl: 'http://example.com/?item=1', +// path: '/', +// searchParams: { item: '1' }, +// protocol: 'http:', +// host: 'example.com', +// port: '' +// } +``` + +#### Example - access call history on intercepted client + +You can use `registerCallHistory` to register a specific MockCallHistory instance while you are intercepting request. This is useful to have an history already filtered. Note that `getCallHistory()` will still register every request configuration. + +```js +import { MockAgent, setGlobalDispatcher, request } from 'undici' + +const mockAgent = new MockAgent() +setGlobalDispatcher(mockAgent) + +const client = mockAgent.get('http://example.com') + +client.intercept({ path: '/', method: 'GET' }).reply(200, 'hi !').registerCallHistory('my-specific-history-name') + +await request('http://example.com') // intercepted +await request('http://example.com', { method: 'POST', body: JSON.stringify({ data: 'hello' }), headers: { 'content-type': 'application/json' }}) + +mockAgent.getCallHistory('my-specific-history-name').calls() +// Returns [ +// MockCallHistoryLog { +// body: undefined, +// headers: undefined, +// method: 'GET', +// origin: 'http://example.com', +// fullUrl: 'http://example.com/', +// path: '/', +// searchParams: {}, +// protocol: 'http:', +// host: 'example.com', +// port: '' +// } +// ] + +mockAgent.getCallHistory().calls() +// Returns [ +// MockCallHistoryLog { +// body: undefined, +// headers: undefined, +// method: 'GET', +// origin: 'http://example.com', +// fullUrl: 'http://example.com/', +// path: '/', +// searchParams: {}, +// protocol: 'http:', +// host: 'example.com', +// port: '' +// }, +// MockCallHistoryLog { +// body: "{ "data": "hello" }", +// headers: { 'content-type': 'application/json' }, +// method: 'POST', +// origin: 'http://example.com', +// fullUrl: 'http://example.com/', +// path: '/', +// searchParams: {}, +// protocol: 'http:', +// host: 'example.com', +// port: '' +// } +// ] +``` + +#### Example - clear call history + +Clear all call history registered : + +```js +const mockAgent = new MockAgent() + +mockAgent.clearAllCallHistory() +``` + +Clear only one call history : + +```js +const mockAgent = new MockAgent() + +mockAgent.getCallHistory().clear() +mockAgent.getCallHistory('my-history')?.clear() +``` + +#### Example - call history instance class method + +```js +const mockAgent = new MockAgent() + +const mockAgentHistory = mockAgent.getCallHistory() + +mockAgentHistory.calls() // returns an array of MockCallHistoryLogs +mockAgentHistory.firstCall() // returns the first MockCallHistoryLogs or undefined +mockAgentHistory.lastCall() // returns the last MockCallHistoryLogs or undefined +mockAgentHistory.nthCall(3) // returns the third MockCallHistoryLogs or undefined +``` diff --git a/docs/docs/best-practices/mocking-request.md b/docs/docs/best-practices/mocking-request.md index 68831931ae8..95cdb124be2 100644 --- a/docs/docs/best-practices/mocking-request.md +++ b/docs/docs/best-practices/mocking-request.md @@ -75,6 +75,73 @@ assert.deepEqual(badRequest, { message: 'bank account not found' }) Explore other MockAgent functionality [here](/docs/docs/api/MockAgent.md) +## Access agent history + +Using a MockAgent also allows you to make assertions on the configuration used to make your http calls in your application. + +Here is an example : + +```js +// index.test.mjs +import { strict as assert } from 'assert' +import { MockAgent, setGlobalDispatcher, fetch } from 'undici' +import { app } from './app.mjs' + +// given an application server running on http://localhost:3000 +await app.start() + +const mockAgent = new MockAgent() + +setGlobalDispatcher(mockAgent) + +// this call is made (not intercepted) +await fetch(`http://localhost:3000/endpoint?query='hello'`, { + method: 'POST', + headers: { 'content-type': 'application/json' } + body: JSON.stringify({ data: '' }) +}) + +// access to the call history of the MockAgent (which register every call made intercepted or not) +assert.ok(mockAgent.getCallHistory().calls().length === 1) +assert.strictEqual(mockAgent.getCallHistory().firstCall()?.fullUrl, `http://localhost:3000/endpoint?query='hello'`) +assert.strictEqual(mockAgent.getCallHistory().firstCall()?.body, JSON.stringify({ data: '' })) +assert.deepStrictEqual(mockAgent.getCallHistory().firstCall()?.searchParams, { query: 'hello' }) +assert.strictEqual(mockAgent.getCallHistory().firstCall()?.port, '3000') +assert.strictEqual(mockAgent.getCallHistory().firstCall()?.host, 'localhost:3000') +assert.strictEqual(mockAgent.getCallHistory().firstCall()?.method, 'POST') +assert.strictEqual(mockAgent.getCallHistory().firstCall()?.path, '/endpoint') +assert.deepStrictEqual(mockAgent.getCallHistory().firstCall()?.headers, { 'content-type': 'application/json' }) + +// register a specific call history for a given interceptor (useful to filter call within a particular interceptor) +const mockPool = mockAgent.get('http://localhost:3000'); + +// we intercept a call and we register a specific MockCallHistory +mockPool.intercept({ + path: '/second-endpoint', +}).reply(200, 'hello').registerCallHistory('second-endpoint-history') + +assert.ok(mockAgent.getCallHistory().calls().length === 2) // MockAgent call history has registered the call too +assert.ok(mockAgent.getCallHistory('second-endpoint-history')?.calls().length === 1) +assert.strictEqual(mockAgent.getCallHistory('second-endpoint-history')?.firstCall()?.path, '/second-endpoint') +assert.strictEqual(mockAgent.getCallHistory('second-endpoint-history')?.firstCall()?.method, 'GET') + +// clearing all call history + +mockAgent.clearAllCallHistory() + +assert.ok(mockAgent.getCallHistory().calls().length === 0) +assert.ok(mockAgent.getCallHistory('second-endpoint-history')?.calls().length === 0) + +// clearing a particular history + +mockAgent.getCallHistory().clear() // second-endpoint-history will not be cleared +mockAgent.getCallHistory('second-endpoint-history').clear() // it is not cleared +``` + +Calling `mockAgent.close()` will automatically clear and delete every call history for you. + +Explore other MockAgent functionality [here](/docs/docs/api/MockAgent.md) + ## Debug Mock Value When the interceptor and the request options are not the same, undici will automatically make a real HTTP request. To prevent real requests from being made, use `mockAgent.disableNetConnect()`: diff --git a/lib/mock/mock-call-history.js b/lib/mock/mock-call-history.js index 5e5e5d22e4b..85a241080a4 100644 --- a/lib/mock/mock-call-history.js +++ b/lib/mock/mock-call-history.js @@ -6,6 +6,7 @@ const { kMockCallHistoryClearAll, kMockCallHistoryDeleteAll } = require('./mock-symbols') +const { InvalidArgumentError } = require('../core/errors') const computingError = 'error occurred when computing MockCallHistoryLog.url' @@ -96,7 +97,18 @@ class MockCallHistory { } nthCall (number) { - return this.logs.at(number) + if (typeof number !== 'number') { + throw new InvalidArgumentError('nthCall must be called with a number') + } + if (!Number.isInteger(number)) { + throw new InvalidArgumentError('nthCall must be called with an integer') + } + if (Math.sign(number) !== 1) { + throw new InvalidArgumentError('nthCall must be called with a positive value. use firstCall or lastCall instead') + } + + // non zero based index. this is more human readable + return this.logs.at(number - 1) } clear () {