Mocking Node-Fetch with Jest, Sinon and Typescript
Maximilian
Posted on November 9, 2021
TLDR
If you don't care about the context of these tests and want to go straight to the node-fetch
mocks, skip to the here's my solution section.
Introduction
I wrote a middleware library to be used by micro-services that decodes and verifies JWTs and works with Express and Koa. The requirement was for the library to make an API request to an external service in order to refresh tokens if the provided token had expired. I'm not writing this post to discuss the library itself, but to talk about how I wrote the unit tests for it as I found it a little tricky to implement a solution that catered for sending and receiving dynamic data to and from the external service, whilst keeping the tests isolated. Hopefully this will be helpful to someone trying to do a similar thing.
The middleware
The controller function looks a little something like this:
async function checkToken(
reqHeaders: IncomingHttpHeaders
): Promise<ITokenData> {
// Get access token from auth header
const accessToken = reqHeaders.authorization?.split(/\s+/)[1];
// Decode token
const decodedToken = await verifyAndDecodeToken(accessToken, SECRET);
// Token is valid, return the decoded token
if (decodedToken.exp > Date.now() / 1000) return decodedToken.tokenData;
// Store the data from the decoded token in a variable
const tokenData: ITokenData = decodeToken.tokenData;
// Call the external API using the data decoded from the access token
const newAccessToken = await refreshTokens(tokenData);
// Decode token returned from external API
const decodedNewToken = await verifyAndDecodeToken(newAccessToken, SECRET);
// Return the decoded new token
return checkNewToken.tokenData;
}
The refreshTokens()
function looks something like this:
async function refreshTokens(
tokenData: ITokenData
): Promise<string | undefined> {
const res = await fetch(`https://refreshmytokensyouslag.com`, {
method: `post`,
body: JSON.stringify({ tokenData }),
headers: {
"content-type": `application/json`,
},
});
const resJson = await res.json();
return resJson?.data.newAccessToken;
}
And, just for the sake of context, the wrapper functions (or 'factories') for Koa and Express look something like this:
/**
* Middleware factory for Express
*/
function checkTokenExpress() {
return async function checkTokenMiddleware(
req: express.Request,
res: express.Response,
next: express.NextFunction
): Promise<void> {
const decodedToken = await checkToken(req.headers);
req.userData = decodedToken;
return void next();
};
}
/**
* Middleware factory for Koa
*/
function checkTokenKoa() {
return async function checkTokenMiddleware(
ctx: Koa.Context,
next: Koa.Next
): Promise<void> {
const decodedToken = await checkToken(ctx.headers);
ctx.userData = decodedToken;
await next();
};
}
Code explanation
We have our 2 'middleware factories'; one for Express and one for Koa. Both are exported, ready to be used in any other Express or Koa services as middleware. Both factories call the checkToken()
function, append a decoded token to the req
or ctx
objects respectively, then call next()
.
Our controller function, checkToken()
, verifies and decodes access tokens. If the token is valid and hasn't expired, it returns the decoded token object. If the token is invalid, it will throw an error, and if the token is valid but has expired, it calls the refreshTokens()
function.
The refreshTokens()
function makes a request to an external API which handles the issuing of new access tokens if certain conditions are met. Our checkToken()
function will then decode and return this new token.
The tests
Testing for the token being valid was pretty simple as the code is already isolated. Here's what the code looks like for both the Koa and Express implementations:
// Express
test(`middleware calls next if access token is valid`, async () => {
// Create a token to test
const testAccessToken = jwt.sign({ foo: `bar` }, SECRET, {
expiresIn: `1h`,
});
// Mock the request object
const mockReq = {
headers: { authorization: `Bearer ${testAccessToken}` },
};
// Mock the response object
const mockRes = {};
const req = mockReq as unknown as ExpressRequest;
const res = mockRes as unknown as ExpressResponse;
// Mock the next() function
const next = Sinon.stub();
// Invoke Express
const middleware = express(SECRET);
void (await middleware(req, res, next));
// Our test expectation
expect(next.callCount).toBe(1);
});
// Koa
test(`middleware calls next if access token is valid`, async () => {
// Create a token to test
const testAccessToken = jwt.sign({ foo: `bar` }, SECRET, {
expiresIn: `1h`,
});
// Mock the ctx object
const mockCtx = {
headers: { authorization: `Bearer ${testAccessToken}` },
};
const ctx = mockCtx as unknown as KoaContext;
// Mock the next() function
const next = Sinon.stub();
// Invoke Koa
const middleware = koa(SECRET);
void (await middleware(ctx, next));
// Our test expectation
expect(next.callCount).toBe(1);
});
Code explanation
The tests for Express and Koa are nearly identical, we just have to cater for Express' request
object and Koa's ctx
object.
In both tests, we're creating a valid token testAccessToken
and mocking the next()
functions with Sinon. We're then mocking the request
and response
objects for Express, and the ctx
object for Koa. After that, we're invoking the middleware and telling Jest that we expect the next()
function to be called once, i.e. we're expecting the token to be valid and the middleware to allow us to progress to the next step in our application.
What does a test for a failure look like?
From this point onwards, I'll only give code examples in Koa as there's slightly less code to read through, but you should have no problem adapting it for Express using the examples above.
test(`middleware throws error if access token is invalid`, async () => {
const testAccessToken = `abcd1234`;
const mockCtx = {
headers: { authorization: `Bearer ${testAccessToken}` },
};
const ctx = mockCtx as unknown as KoaContext;
const next = Sinon.stub();
const middleware = koa(SECRET, API_URI);
await expect(middleware(ctx, next)).rejects.toThrowError(
/access token invalid/i
);
});
Code explanation
Here, we're creating a testAccessToken
that is just a random string, and giving it to our middleware. In this case, we're expecting the middleware to throw an error that matches the regular expression, access token invalid
. The rest of the logic in this test is the same as the last one, in that we're just mocking our ctx
object and next
function.
The tricky bit: testing dynamic calls to an external API
We always need tests to run in isolation. There are several reasons for this, but the main one is that we're not interested in testing anything that's not a part of our code, and therefore outside of our control.
So the question is, how can we dynamically test for different responses from an external API or service?
First, we mock the node-fetch
library, which means that any code in the function we test that uses node-fetch
is mocked. Next, in order to make the responses dynamic, we create a variable that we can assign different values to depending on what we're testing. We then get our mocked node-fetch
function to return a function, which mocks the response object provided by Express and Koa.
That's a bit of a mouth full. So let's look at some code...
Here's my solution
At the top of my .spec
file, we have the following (in JS to make it easier to read):
// The variable we can change for different tests
let mockTokenFromAPI;
// Mocking the 'node-fetch' library
jest.mock(`node-fetch`, () => {
// The function we want 'node-fetch' to return
const generateResponse = () => {
// Mocking the response object
return { json: () => ({ data: { newAccessToken: mockTokenFromAPI } }) };
};
// Put it all together, Jest!
return jest.fn().mockResolvedValue(generateResponse());
});
We first get Jest to mock the node-fetch
library by returning a function. We then get the mocked library to return another function called generateResponse()
. The purpose of generateResponse
is to mock the response objects in Express and Koa, so it returns an object with the json
key. The value of json
is a function, thus mocking the .json()
method, which finally returns the data structure we're expecting from the API, using our mockTokenFromApi
variable. So now in order to make the whole thing dynamic, all we have to do in our tests is change the value of this variable!
Let's Typescript this up...
interface IJsonResponse {
data: {
newAccessToken: string | undefined;
};
}
interface IResponse {
json: () => IJsonResponse;
}
let mockTokenFromAPI: string | undefined;
jest.mock(`node-fetch`, () => {
const generateResponse = (): IResponse => {
return {
json: (): IJsonResponse => ({
data: { newAccessToken: mockTokenFromAPI },
}),
};
};
return jest.fn().mockResolvedValue(generateResponse());
});
And now here's how we can test our middleware with dynamic responses from an external API using the node-fetch
library:
test(`Middleware throws error if refresh token errors`, async () => {
// Create an expired but valid access token to send
const testAccessToken = jwt.sign({ tokenData: { authId: `1234` } }, SECRET, {
expiresIn: `0`,
});
// DYNAMICALLY SET WHAT WE WANT THE EXTERNAL API / SERVICE TO RETURN
// In this case, an invalid token
mockTokenFromAPI = `abc123`;
const mockCtx = {
headers: { authorization: `Bearer ${testAccessToken}` },
};
const ctx = mockCtx as unknown as KoaContext;
const next = Sinon.stub();
const middleware = koa(SECRET, API_URI);
await expect(middleware(ctx, next)).rejects.toThrowError(
/refresh token error/i
);
});
test(`Middleware calls next if refresh token exists and is valid`, async () => {
// Create an expired but valid access token to send
const testAccessToken = jwt.sign({ tokenData: { authId: `1234` } }, SECRET, {
expiresIn: `0`,
});
// DYNAMICALLY SET WHAT WE WANT THE EXTERNAL API / SERVICE TO RETURN
// In this case, a valid token
mockTokenFromAPI = jwt.sign({ tokenData: { authId: `1234` } }, SECRET, {
expiresIn: `1h`,
});
const mockCtx = {
headers: { authorization: `Bearer ${testAccessToken}` },
};
const ctx = mockCtx as unknown as KoaContext;
const next = Sinon.stub();
const middleware = koa(SECRET, API_URI);
void (await middleware(ctx, next));
expect(next.callCount).toBe(1);
});
Conclusion
We now have the ability to get 100% isolated test coverage on our middleware, even though it relies on an external API.
I hope this helped you in some way, and if it didn't, I hope you learned something or at least found it interesting!
Posted on November 9, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.