Mocking Node-Fetch with Jest, Sinon and Typescript
Tags: testing, typescript, javascript, tutorial
Published: 2021-11-09 23:50:10

## 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:

```typescript
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:

```typescript
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:

```typescript
/**
 * 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:

```typescript
// 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.

```typescript
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):

```javascript
// 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...

```typescript
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:

```typescript
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!