Jest: tests can't fail within setImmediate or process.nextTick callback

对着背影说爱祢 提交于 2020-12-29 06:28:59

问题


I'm trying to write a test for a React component that needs to complete an asynchronous action in its componentWillMount method. componentWillMount calls a function, passed as a prop, which returns a promise, and I mock this function in my test.

This works fine, but if a test fails in a call to setImmediate or process.nextTick, the exception isn't handled by Jest and it exits prematurely. Below, you can see I even try to catch this exception, to no avail.

How can I use something like setImmediate or nextTick with Jest? The accepted answer to this question is what I'm trying to implement unsuccessfully: React Enzyme - Test `componentDidMount` Async Call.

it('should render with container class after getting payload', (done) => {
  let resolveGetPayload;
  let getPayload = function() {
    return new Promise(function (resolve, reject) {
      resolveGetPayload = resolve;
    });
  }
  const enzymeWrapper = mount(<MyComponent getPayload={getPayload} />);

  resolveGetPayload({
    fullname: 'Alex Paterson'
  });

  try {
    // setImmediate(() => {
    process.nextTick(() => {
      expect(enzymeWrapper.hasClass('container')).not.toBe(true); // Should and does fail
      done();
    });
  } catch (e) {
    console.log(e); // Never makes it here
    done(e);
  }
});

Jest v18.1.0

Node v6.9.1


回答1:


Another potentially cleaner solution, using async/await and leveraging the ability of jest/mocha to detect a returned promise:

function currentEventLoopEnd() {
  return new Promise(resolve => setImmediate(resolve));
}

it('should render with container class after getting payload', async () => {
  let resolveGetPayload;
  let getPayload = function() {
    return new Promise(function (resolve, reject) {
      resolveGetPayload = resolve;
    });
  }
  const enzymeWrapper = mount(<MyComponent getPayload={getPayload} />);

  resolveGetPayload({
    fullname: 'Alex Paterson'
  });

  await currentEventLoopEnd(); // <-- clean and clear !

  expect(enzymeWrapper.hasClass('container')).not.toBe(true);
});



回答2:


Overcome that issue in the next way atm (also it solves the issue with Enzyme and async call in componentDidMount and async setState):

it('should render proper number of messages based on itemsPerPortion', (done) => {
  const component = shallow(<PublishedMessages itemsPerPortion={2} messagesStore={mockMessagesStore()} />);

  setImmediate(() => { // <-- that solves async setState in componentDidMount
    component.update();

    try { // <-- that solves Jest crash
      expect(component.find('.item').length).toBe(2);
    } catch (e) {
      return fail(e);
    }

    done();
  });
});

(Enzyme 3.2.0, Jest 21.1.6)

UPDATE

Just figured out another, better (but still strange) solution on that using async/await (and it still solving async componentDidMount and async setState):

it('should render proper number of messages based on itemsPerPortion', async () => {
  // Magic below is in "await", looks as that allows componentDidMount and async setState to complete
  const component = await shallow(<PublishedMessages itemsPerPortion={2} messagesStore={mockMessagesStore()} />);

  component.update(); // still needed
  expect(component.find('.item').length).toBe(2);
});

Also other async-related actions should be prefixed with await as well like

await component.find('.spec-more-btn').simulate('click');



回答3:


Some things to note;

  • process.nextTick is async, so the try/catch won't be able to capture that.
  • Promise will also resolve/reject async even if the code you run in the Promise is sync.

Give this a try

it('should render with container class after getting payload', (done) => {
    const getPayload = Promise.resolve({
        fullname: 'Alex Paterson'
    });
    const enzymeWrapper = mount(<MyComponent getPayload={getPayload} />);

    process.nextTick(() => {
        try {
            expect(enzymeWrapper.hasClass('container')).not.toBe(true);
        } catch (e) {
            return done(e);
        }
        done();
    });
});



回答4:


Following on from Vladimir's answer + edit, here's an alternative that worked for me. Rather than await the mount, await the wrapper.update():

it('...', async () => {

  let initialValue;
  let mountedValue;

  const wrapper = shallow(<Component {...props} />);
  initialValue = wrapper.state().value;

  await wrapper.update(); // componentDidMount containing async function fires
  mountedValue = wrapper.state().value;

  expect(mountedValue).not.toBe(initialValue);
});



回答5:


Wrapping the callback block passed to process.nextTick or setImmediate in a try/catch works, as others have shown, but this is verbose and distracting.

A cleaner approach is to flush promises using the brief line await new Promise(setImmediate); inside an async test callback. Here's a working example of using this to let an HTTP request in a useEffect (equally useful for componentDidMount) resolve and trigger a re-render before running assertions:

Component (LatestGist.js):

import axios from "axios";
import {useState, useEffect} from "react";

export default () => {
  const [gists, setGists] = useState([]);

  const getGists = async () => {
    const res = await axios.get("https://api.github.com/gists");
    setGists(res.data);
  };    
  useEffect(() => getGists(), []);

  return (
    <>
      {gists.length
        ? <div data-test="test-latest-gist">
            the latest gist was made on {gists[0].created_at} by {gists[0].owner.login}
          </div>
        : <div>loading...</div>}
    </>
  );
};

Test (LatestGist.test.js):

import React from "react";
import {act} from "react-dom/test-utils";
import Enzyme, {mount} from "enzyme";
import Adapter from "enzyme-adapter-react-16";
Enzyme.configure({adapter: new Adapter()});
import mockAxios from "axios";
import LatestGist from "./LatestGist";

jest.mock("axios");

describe("LatestGist", () => {
  beforeEach(() => jest.resetAllMocks());
  
  it("should load the latest gist", async () => {
    mockAxios.get.mockImplementationOnce(() => Promise.resolve({
      data: [{owner: {login: "test name"}, created_at: "some date"}],
      status: 200
    }));

    const wrapper = mount(<LatestGist />);
    let gist = wrapper.find('[data-test="test-latest-gist"]');
    expect(gist.exists()).toBe(false);

    await act(() => new Promise(setImmediate));
    wrapper.update();

    expect(mockAxios.get).toHaveBeenCalledTimes(1);
    gist = wrapper.find('[data-test="test-latest-gist"]');
    expect(gist.exists()).toBe(true);
    expect(gist.text()).toContain("test name");
    expect(gist.text()).toContain("some date");
  });
});

Forcing a failed assertion with a line like expect(gist.text()).toContain("foobar"); doesn't cause the suite to crash:

● LatestGist › should load the latest gist

expect(string).toContain(value)

  Expected string:
    "the latest gist was made on some date by test name"
  To contain value:
    "foobar"

    at Object.it (src/LatestGist.test.js:30:25)



回答6:


-- Posting as an answer for one cannot format code blocks in comments. --

Building upon Vladimir's answer, note that using async/await works in a beforeEach as well:

var wrapper

beforeEach(async () => {
  // Let's say Foobar's componentDidMount triggers async API call(s)
  // resolved in a single Promise (use Promise.all for multiple calls).
  wrapper = await shallow(<Foobar />)
})

it('does something', () => {
  // no need to use an async test anymore!
  expect(wrapper.state().asynchronouslyLoadedData).toEqual(…)
})


来源:https://stackoverflow.com/questions/41792927/jest-tests-cant-fail-within-setimmediate-or-process-nexttick-callback

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!