DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’» is a community of 964,423 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Maximilian
Maximilian

Posted on

Mocking Node-Fetch with Jest, Sinon and Typescript

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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();
    };
}
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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
    );
});
Enter fullscreen mode Exit fullscreen mode

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());
});
Enter fullscreen mode Exit fullscreen mode

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());
});
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)

Classic DEV Post from 2020:

js visualized

πŸš€βš™οΈ JavaScript Visualized: the JavaScript Engine

As JavaScript devs, we usually don't have to deal with compilers ourselves. However, it's definitely good to know the basics of the JavaScript engine and see how it handles our human-friendly JS code, and turns it into something machines understand! πŸ₯³

Happy coding!