found icon indicating copy to clipboard operation
found copied to clipboard

Mock `router` to test component using Jest

Open gabrielgian opened this issue 7 years ago • 24 comments

I want to test my components with Snapshots and am using Jest to do so. Since my component is using withRoute HOC, both my component and somewher in HOC require router and all its functions in their in the contexts.

Is there a easy way to create an instance of router or mock it?

Here is a sample of my code:

const initialState = store.getState();
const router = {}; // ???

test('Login changes', () => {
  const wrapper = shallow(<Login />, {
    context: { store: mockStore(initialState), router }
  });
  expect(wrapper.dive()).toMatchSnapshot();
}); 

gabrielgian avatar Oct 24 '17 12:10 gabrielgian

We use a mock object where we replace all the functions with sinon.spy(). If you're using Jest's mocking, then you can just jest.fn(). You may need trivial implementations of createHref and createLocation, though.

Does Jest have like "smart" mocks that use object proxies or anything?

taion avatar Oct 24 '17 15:10 taion

jest will auto mock something intelligently i think based on module exports. e.g. jest.mock('found/router')

jquense avatar Oct 24 '17 15:10 jquense

I don't think we export the object anywhere, though. We do export the prop type. Python has "smart" mocks that are essentially proxy objects with all methods auto-mocked. Does such a thing exist in the Node ecosystem?

taion avatar Oct 24 '17 15:10 taion

i think jest is the best example of it, but it still a bit welded into jest itself. there might be some better optins tho now that Proxy is well supported

jquense avatar Oct 24 '17 15:10 jquense

I was able to mock 'found' module by doing jest.mock('found'), but couldn't the router itself.

Following @taion comment the test passed (although it seems to me like a temporary solution) by defining router as:

const router = {
  push: jest.fn(),
  replace: jest.fn(),
  go: jest.fn(),
  createHref: jest.fn(),
  createLocation: jest.fn(),
  isActive: jest.fn(),
  matcher: {
    match: jest.fn(),
    getRoutes: jest.fn(),
    isActive: jest.fn(),
    format: jest.fn()
  },
  addTransitionHook: jest.fn()
};

gabrielgian avatar Oct 24 '17 15:10 gabrielgian

I guess we could expose like a found/testing that has helpers for building mocks like that.

taion avatar Oct 24 '17 15:10 taion

I can make a PR if you decide to do so. Just want to know which are the objects you want me to expose.

Another test related issue I faced was to get an initial state of found to initialize a mockStore configured by redux-mock-store. This issue might be addressed in #91, but instead of server rendering, I created a new Store only for testing. I am not sure if that is the best solution too.

gabrielgian avatar Oct 24 '17 17:10 gabrielgian

Well, if you're trying to test state management, it might be easiest to use a server protocol and just actually instantiate Found.

taion avatar Oct 24 '17 21:10 taion

No problems with Found instance so. TY again @taion .

gabrielgian avatar Oct 26 '17 15:10 gabrielgian

For those trying to test components that depend on router, here is what I ended up doing.

export function renderWithRouter(Component, props) {  
  const config = makeRouteConfig(
      <Route path='/'
        render={() => React.createElement(Component, props)}
      />
  );

  // Now make a request against the route matching this route.
  return getFarceResult({ // promise
    url: "/",
    routeConfig: config,
    render: createRender({})
  }).then(result => renderer.create(result.element));
}
...
it('snapshot', () => {
  // Catch a rejected promise by looking for a certain number of assertions
  expect.assertions(1);

  return renderWithRouter(LeftSidePanel).then(tree => {
    expect(tree.toJSON()).toMatchSnapshot()
  })
});

cmutzel avatar Nov 20 '17 18:11 cmutzel

Okay, I think the thing to do here is to add a found/testing or found/test-utils module that wraps up some of these test utils for convenience.

taion avatar Nov 20 '17 18:11 taion

Along these lines, I'm trying to enzyme mount and need the router. Since I'm using a link, it fails with the simple spy-based router. So +1 for exposing a test utility.

rosskevin avatar Jan 16 '18 18:01 rosskevin

Was there a fix for this? I have tried @gabrielgian suggestion of mocking the router object with with jest.fn() I was then faced with similar issues for farce which I also mocked but still having problems in the found/lib/createWithRouter.js:24 with found being undefined

wkerswell-gresham avatar Apr 06 '18 13:04 wkerswell-gresham

@gabrielgian do you have an example you could share or something working?

wkerswell-gresham avatar Apr 06 '18 13:04 wkerswell-gresham

So I ended up mocking the <Link/> component using jest. This fixed the error and produced an isolated component no dependency on the result of the link.

// Mock the nested "connected" components to avoid error with missing context.
jest.mock('found/lib/Link', () => 'link');

wkerswell-gresham avatar Apr 10 '18 08:04 wkerswell-gresham

That makes sense to me.

taion avatar Apr 10 '18 15:04 taion

Guys, I want to test this component that is use in my app inside a withRouter( connect(

what is the best way to test it with as little boilerplate code?

  componentWillMount() {
    const { issueDetails, loadIssueDetails, historyTypes, loadLookupData, match } = this.props;

    if (match.params.issueId === 'create' && issueDetails === undefined) {
      if (!this.props.siteId) {
        this.props.history.push('/')

I would rather use mount, than doing something like this in the test

     <Route path='/'
        render={() => React.createElement(Component, props)}
      />
  );```

quantuminformation avatar Mar 26 '19 11:03 quantuminformation

There's not a really good way to do this at the moment. I'm going to expose the router context as its own thing, and then it will be possible to inject a test router, but right now the best bet is to follow the paths above – either render a server/memory router.

Though if you're actually testing navigation, arguably it'd be best to use MemoryProtocol anyway rather than using a mock.

taion avatar Mar 26 '19 22:03 taion

Okay, plan here is to add a dummyRouter export in found/lib/test-utils (and a Jest-specific version as well).

taion avatar Apr 05 '19 14:04 taion

dummyRouter sounds like a good solution!

having trouble implementing the above solutions so one a bit more simple is something to look forward to!

MatthewMSaucedo avatar Aug 21 '19 17:08 MatthewMSaucedo

As an update, I found it much easier to just make a MockRouter class with just the methods I needed. Then in a test,

const mockRouter = new MockRouter() as unknown as Router;

Worked fine for my use cases.

MatthewMSaucedo avatar Aug 21 '19 19:08 MatthewMSaucedo

Any update on this?

renanmav avatar Oct 31 '19 16:10 renanmav

For anyone who uses jest, this has been working for us:

Re anything that returns a <BaseLink /> error, just add this to your test file:

jest.mock('found/BaseLink');

Re anything context related that probably stems from useRouter, use this:

jest.mock('found/useRouter', () => {
  return function useRouter() {
    return {
      router: {
        addNavigationListener: jest.fn(),
        createHref: jest.fn(),
        createLocation: jest.fn(),
        go: jest.fn(),
        isActive: jest.fn(),
        matcher: {
          match: jest.fn(),
          getRoutes: jest.fn(),
          isActive: jest.fn(),
          format: jest.fn(),
        },
        push: jest.fn(),
        replace: jest.fn(),
        replaceRouterConfig: jest.fn(),
      },
      match: {
        context: undefined,
        location: {
          action: '',
          delta: 0,
          hash: '',
          index: 0,
          key: '',
          pathname: '',
          query: {},
          search: '',
          state: undefined,
        },
        params: {
          org: '',
          view: '',
        },
        route: {},
        routeIndices: [],
        routeParams: null,
        routes: [],
      },
    };
  };
});

caseybaggz avatar Oct 12 '20 17:10 caseybaggz

You can mock your react-router with the code below. Let's say all you needed in your component is Redirect from router-dom. You can put the code below before describe block

jest.mock("react-router-dom", () => ({ Redirect: jest.fn(() => null) }));

mos-adebayo avatar Nov 22 '21 13:11 mos-adebayo