6

Here's my lazy component:

const LazyBones = React.lazy(() => import('@graveyard/Bones')
  .then(module => ({default: module.BonesComponent}))
export default LazyBones

I'm importing it like this:

import Bones from './LazyBones'

export default () => (
<Suspense fallback={<p>Loading bones</p>}>
  <Bones />
</Suspense>
)

And in my test I have this kind of thing:

import * as LazyBones from './LazyBones';

describe('<BoneYard />', function() {
  let Bones;
  let wrapper;
  beforeEach(function() {
    Bones = sinon.stub(LazyBones, 'default');
    Bones.returns(() => (<div />));
    wrapper = shallow(<BoneYard />);
  });
  afterEach(function() {
    Bones.restore();
  });

  it('renders bones', function() {
    console.log(wrapper)
    expect(wrapper.exists(Bones)).to.equal(true);
  })

})

What I expect is for the test to pass, and the console.log to print out:

<Suspense fallback={{...}}>
  <Bones />
</Suspense>

But instead of <Bones /> I get <lazy /> and it fails the test.

How can I mock out the imported Lazy React component, so that my simplistic test passes?

AncientSwordRage
  • 7,086
  • 19
  • 90
  • 173

4 Answers4

7

I'm not sure this is the answer you're looking for, but it sounds like part of the problem is shallow. According to this thread, shallow won't work with React.lazy.

However, mount also doesn't work when trying to stub a lazy component - if you debug the DOM output (with console.log(wrapper.debug())) you can see that Bones is in the DOM, but it's the real (non-stubbed-out) version.

The good news: if you're only trying to check that Bones exists, you don't have to mock out the component at all! This test passes:

import { Bones } from "./Bones";
import BoneYard from "./app";

describe("<BoneYard />", function() {
  it("renders bones", function() {
    const wrapper = mount(<BoneYard />);
    console.log(wrapper.debug());
    expect(wrapper.exists(Bones)).to.equal(true);
    wrapper.unmount();
  });
});

If you do need to mock the component for a different reason, jest will let you do that, but it sounds like you're trying to avoid jest. This thread discusses some other options in the context of jest (e.g. mocking Suspense and lazy) which may also work with sinon.

helloitsjoe
  • 6,264
  • 3
  • 19
  • 32
  • Unfortunately, the real bones component has to be stubbed as it contains an es6 import. But this is a good answer for everyone else. – AncientSwordRage Jan 30 '20 at 03:29
2

You don't need to resolve lazy() function by using .then(x => x.default) React already does that for you.

React.lazy takes a function that must call a dynamic import(). This must return a Promise which resolves to a module with a default export containing a React component. React code splitting

Syntax should look something like:

const LazyBones = React.lazy(() => import("./LazyBones"))

Example:

// LazyComponent.js
import React from 'react'

export default () => (
  <div>
    <h1>I'm Lazy</h1>
    <p>This component is Lazy</p>
  </div>
)

// App.js
import React, { lazy, Suspense } from 'react'
// This will import && resolve LazyComponent.js that located in same path
const LazyComponent = lazy(() => import('./LazyComponent'))

// The lazy component should be rendered inside a Suspense component
function App() {
  return (
    <div className="App">
      <Suspense fallback={<p>Loading...</p>}>
        <LazyComponent />
      </Suspense>
    </div>
  )
}

Edit react-lazy-component-test


As for Testing, you can follow the React testing example that shipped by default within create-react-app and change it a little bit.

Create a new file called LazyComponent.test.js and add:

// LazyComponent.test.js
import React, { lazy, Suspense } from 'react'
import { render, screen } from '@testing-library/react'

const LazyComponent = lazy(() => import('./LazyComponent'))

test('renders lazy component', async () => {
  // Will render the lazy component
  render(
    <Suspense fallback={<p>Loading...</p>}>
      <LazyComponent />
    </Suspense>
  )
  // Match text inside it
  const textToMatch = await screen.findByText(/I'm Lazy/i)
  expect(textToMatch).toBeInTheDocument()
})

Live Example: Click on the Tests Tab just next to Browser tab. if it doesn't work, just reload the page.

Edit react-lazy-component-testing

You can find more react-testing-library complex examples at their Docs website.

awran5
  • 4,333
  • 2
  • 15
  • 32
  • Is this not possible using sinon? I'd rather not have to convince my team to change 1000+ tests so I can cover one thing. – AncientSwordRage Jan 24 '20 at 12:28
  • Sure it's possible, those examples are just to get the idea, you can follow the same principle and do more complex ones. maybe [this](https://codesandbox.io/s/distracted-shape-g0dvs?fontsize=14&module=%2Fsrc%2Fapp.spec.js&previewwindow=tests) will help a bit. – awran5 Jan 24 '20 at 12:39
  • I've had a look and I can't find an equivalent in sinon. As such 'Use a different testing library' is not useful. – AncientSwordRage Jan 27 '20 at 18:31
  • @Pureferret you can use directly the jest.spyOn to spy behavior of object like jest.spyOn(props, 'onChange'). Otherwise, keep in mind that the jest.fn() return a object with properties like sinon. – xargr Jan 29 '20 at 12:22
  • @xargr I'm not using jest (unfortunately). And this answer doesn't involve jest. I'm not sure why you're bringing it up? – AncientSwordRage Jan 29 '20 at 13:56
0

I needed to test my lazy component using Enzyme. Following approach worked for me to test on component loading completion:

const myComponent = React.lazy(() => 
      import('@material-ui/icons')
      .then(module => ({ 
         default: module.KeyboardArrowRight 
      })
   )
);

Test Code ->

//mock actual component inside suspense
jest.mock("@material-ui/icons", () => { 
    return {
        KeyboardArrowRight: () => "KeyboardArrowRight",
}
});

const lazyComponent = mount(<Suspense fallback={<div>Loading...</div>}>
           {<myComponent>}
       </Suspense>);
    
const componentToTestLoaded  = await componentToTest.type._result; // to get actual component in suspense
    
expect(componentToTestLoaded.text())`.toEqual("KeyboardArrowRight");

This is hacky but working well for Enzyme library.

ShailyAggarwal
  • 813
  • 9
  • 20
-3

To mock you lazy component first think is to transform the test to asynchronous and wait till component exist like:

import CustomComponent, { Bones } from './Components';

it('renders bones', async () => {
   const wrapper = mount(<Suspense fallback={<p>Loading...</p>}>
                       <CustomComponent />
                   </Suspense>

   await Bones;
   expect(wrapper.exists(Bones)).toBeTruthy();
}
xargr
  • 2,788
  • 1
  • 19
  • 28