Faking MS Graph API Responses
While refactoring and upgrading one of our internal tools that is using the MS Graph API, I experimented with several techniques to fake API responses to simulate requests without actually hitting the MS Graph Production API.
To reduce maintenance overhead, I was looking for a way to avoid creating a separate API service that would return the
mocked responses I need. That would be my fallback method if I would not find a better solution. If you want to go this
route, you can configure a different baseUrl for the requests via the ClientOptions
configuration:
let clientOptions: ClientOptions = {
baseUrl: 'http://mocked-api/'
};
const client = Client.initWithMiddleware(clientOptions);
The Javascript API client comes with support for custom middlewares. A middleware allows you to change the behavior of the client, e.g. you can customize the request or modify the response. That sounded like the perfect way of to return custom responses for my testcases.
I implemented a middleware that gets a list of requests and responses passed. The middleware will check if the current request matches the expected request and then return the response. In case the expected request does not match, an exception is thrown. This is how the implementation looks like:
export class FakeResponseHandlerMiddleware implements Middleware {
private recordedResponses: RecordedResponse[] = [];
/* eslint-disable @typescript-eslint/no-inferrable-types */
private readonly recordedResponseSize: number = 0;
public constructor(responses: RecordedResponse[]) {
this.recordedResponses = responses;
this.recordedResponseSize = responses.length;
}
public async execute(context: Context): Promise<void> {
const recordedResponse = this.recordedResponses.shift();
if((recordedResponse !== undefined) && recordedResponse.matchesRequest(context.request.toString())) {
context.response = recordedResponse.getResponse();
return;
}
const idx: number = this.recordedResponseSize - this.recordedResponses.length;
throw new Error("Request #" + idx + " does not match RecordedRequest or is not defined!");
}
}
The RecordedResponse
object is a value object which contains the expected request url and the response that should be
returned. The implementation looks like this:
export class RecordedResponse
{
private request: string;
private response: Response;
public constructor(request: string, response: Response) {
this.request = request;
this.response = response;
}
public matchesRequest(request: string): boolean {
// endsWith() is needed because request contains the full url https://graph.microsoft.com/v1.0/me not just
// the path like /me
return request.endsWith(this.request)
}
public getResponse(): Response {
return this.response;
}
}
In my code I can now set up all the responses in the order I need them:
const responses: RecordedResponse[] = [];
responses.push(new RecordedResponse("/me", new Response("hi")));
A new MS Graph API client is configured with the FakeResponseHandlerMiddleware
instance:
const clientOptions: ClientOptions = {
middleware: new FakeResponseHandlerMiddleware(responses)
};
const client = Client.initWithMiddleware(clientOptions);
The client
instance can be passed to a service or used directly to return the mocked responses:
const response = await client.api("/me").get();
expect(response).toBe('hi');
So far, I am happy with this technique. If I still continue to like it, we might extract the middleware into a separate package and put it up on GitHub.
For testing purposes, it makes more sense to mock the MS Graph Client API, e.g. by using a library like jest. The technique can be helpful for local development when you want to simulate working with an external API, but you don't want to set up a separate Mock API server.