2

So I am trying to mock a helper function that is nested within other helper functions, using Jest.

The basic structure of the function I actually want to test is like this

import matrixClient from '@mapbox/mapbox-sdk/services/matrix';

// I want to test this function which uses several other helpers
export const getOrderEstimate = async (start, end) => {
  // other non-delivery estimate stuff
  const deliveryEstimate = await getDeliveryEstimate(start, end);

  return {
    ...otherStuff
    delivery: deliveryEstimate 
  }
}

const getDeliveryEstimate = async (start, end) => {
  try {
    return await functionThatWrapsMapBox(start, end);
  } catch(error) {
    // return error
  }
}

const functionThatWrapsMapBox = async (start, end) => {
  return matrixClient({ accessToken: MAPBOX_TOKEN })
    .getMatrix({
      points: [{ coordinates: start }, { coordinates: end }],
      profile: `driving-traffic`,
    })
    .send();
}

In my test file I am trying to do something like this,

import * as helpers from './order-estimates';

describe('order estimates', () => {
  test('delivery estimates should never be null', () => {
    const mockMapBox = jest.spyOn(helpers, 'functionThatWrapsMapBox');
    mockMapBox.mockReturnValue(() => ...mockedReturn))
    // this function always uses the real implementation of functionThatWrapsMapBox
    const orderEstimates = helpers.getOrderEstimate(startAddress, endAddress);
    expect(orderEstimates.delivery).not.toBeNull();
  });
});

I have tried several manual mocking techniques using jest.mock and I can only get the mock implementation to fire if I call it directly in my test file which isn't really what I want. I am thinking the jest.spyOn method is more appropriate but no luck yet.

Justin
  • 683
  • 8
  • 24
  • You can't mock other functions in the same module. You also _shouldn't_, they're an implementation detail (effectively a private method). Test doubles are for _collaborators_, which should be coming from separate modules (in this case presumably `mapboxrequest`). – jonrsharpe Jul 12 '21 at 22:41
  • @jonrsharpe I was a little confused by your comment, but I think what you're saying is that I should really be mocking the mapbox client. I updated the code snippet to better illustrate what is happening. Thanks for the response! – Justin Jul 13 '21 at 16:17
  • I think having a facade around `matrixClient`, which you also don't own, is a good idea. But you'll have to move that facade _outside_ the module to be able to replace it with a test double and test the consumer (`getOrderEstimate`). – jonrsharpe Jul 13 '21 at 16:19
  • Okay, I think I follow. I appreciate your help! I'm gonna see if I can get this working and I will post the answer. – Justin Jul 13 '21 at 16:20
  • I think you just have a dupe of https://stackoverflow.com/q/45111198/3001761 at this point (although many of the answers are disgusting hacks around the fact that you should just extract it from the current module). – jonrsharpe Jul 13 '21 at 16:23
  • @jonrsharpe ahh I see what you mean. So those solutions get it to work but don't address the issue of having the dependency that they're trying to mock as it's own module. I am happy to close this as duplicate as I agree. – Justin Jul 13 '21 at 16:25

1 Answers1

0

The solution that I came up with really mirrors the documentation found here. Essentially, I pulled out the map box client into it's own module adjacent to this helpers module. I then created a mock implementation in the __mocks__ directory. Then in my test, it was as simple as having jest.mock('./mapbox'); at the top of the file and the mock implementation was used. I will now have access to more fine grained control as well.

Summary

First, I extracted the mapbox client into it's own file.

import functionThatWrapsMapBox from './mapbox';

// I want to test this function which uses several other helpers
export const getOrderEstimate = async (start, end) => {
  // other non-delivery estimate stuff
  const deliveryEstimate = await getDeliveryEstimate(start, end);

  return {
    ...otherStuff
    delivery: deliveryEstimate 
  }
}

const getDeliveryEstimate = async (start, end) => {
  try {
    return await functionThatWrapsMapBox(start, end);
  } catch(error) {
    // return error
  }
}

So in, mapbox.js

import matrixClient from '@mapbox/mapbox-sdk/services/matrix';

const functionThatWrapsMapBox = async (start, end) => {
  return matrixClient({ accessToken: MAPBOX_TOKEN })
    .getMatrix({
      points: [{ coordinates: start }, { coordinates: end }],
      profile: `driving-traffic`,
    })
    .send();
}

default export functionThatWrapsMapBox;

and in __mocks__/mapbox.js:

const functionThatWrapsMapBox = async (origin, destination) => {
  return new Promise((resolve, reject) => {
    process.nextTick(() => {
      if (origin && destination) {
        resolve({
          body: {
            durations: [
              [0.0, 135.4],
              [131.5, 0.0],
            ],
          },
        });
      } else {
        reject(new Error('Please provide and origin and a destination'));
      }
    });
  });
};

export default functionThatWrapsMapBox;

and finally in my test:

import { getOrderEstimate } from './order-estimates';

jest.mock('./mapbox');

describe('order estimates', () => {
  test('delivery estimates should never be null', () => {
    // this function uses the mock implementation of functionThatWrapsMapBox
    const orderEstimates = getOrderEstimate(startAddress, endAddress);
    expect(orderEstimates.delivery).not.toBeNull();
  });
});

Huge shoutout to @jonrsharpe for illustrating the correct way to do this!

Justin
  • 683
  • 8
  • 24