Jest:测试不能在 setImmediate 或 process.nextTick 回调中失败

IT技术 node.js reactjs jasmine jestjs
2021-04-11 04:10:37

我正在尝试为需要在其componentWillMount方法中完成异步操作的 React 组件编写测试componentWillMount调用一个函数,作为props传递,它返回一个Promise,我在我的测试中模拟了这个函数。

这工作正常,但如果测试在调用setImmediateor 时失败process.nextTick,则异常不会由 Jest 处理,并且会过早退出。在下面,您可以看到我什至尝试捕获此异常,但无济于事。

我如何使用类似setImmediatenextTick与 Jest 一起使用的东西这个问题的公认答案是我试图实现但未成功的答案: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);
  }
});

玩笑 v18.1.0

节点 v6.9.1

6个回答

另一个可能更干净的解决方案,使用 async/await 并利用 jest/mocha 的能力来检测返回的Promise:

// async test utility function
function currentEventLoopEnd() {
  return new Promise(resolve => setImmediate(resolve));
}

it('should render with container class after getting payload', async () => {
  
  // mock the API call in a controllable way,
  // starts out unresolved
  let resolveGetPayload; // <- call this to resolve the API call
  let getPayload = function() {
    return new Promise(function (resolve, reject) {
      resolveGetPayload = resolve;
    });
  }
  
  // instanciate the component under state with the mock
  const enzymeWrapper = mount(<MyComponent getPayload={getPayload} />);
  expect(enzymeWrapper.hasClass('container')).not.toBe(true);

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

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

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

以 atm 的下一个方式克服这个问题(它还解决了 Enzyme 和 componentDidMount 和 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();
  });
});

(酶 3.2.0,笑话 21.1.6)

更新

刚刚想出了另一个更好(但仍然很奇怪)的解决方案,使用 async/await(它仍然解决 async componentDidMount 和 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);
});

其他与异步相关的操作也应该以前缀await前缀

await component.find('.spec-more-btn').simulate('click');
我也使用了更新的代码await wrapper.update()我注意到有些测试需要await更新,有些则不需要,即使我正在为类似的事情测试相同的组件。
2021-06-16 04:10:37

一些注意事项;

  • process.nextTick 异步的,所以 try/catch 将无法捕获它。
  • Promise 即使您在 Promise 中运行的代码是同步的,也会解析/拒绝异步。

试试这个

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();
    });
});
应该是done.fail(e)如果你这样做done(e),测试将通过,因为done()不带参数。fail函数记录在 Jasmine 页面上:done.fail 函数
2021-05-25 04:10:37
不应该是return fail(e)代替return done(e);吗?
2021-05-27 04:10:37
fail我所知没有方法,只有done回调:facebook.github.io/jest/docs/en/asynchronous.html#callbacks
2021-06-20 04:10:37

继 Vladimir 的回答 + 编辑之后,这里有一个对我有用的替代方法。不是await山,awaitwrapper.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);
});

正如其他人所展示的那样,将传递给/的回调块包装起来process.nextTicksetImmediatetry/ 中catch起作用,但这是冗长且分散注意力的。

一个清洁的方法是使用简单的线齐平的Promiseawait new Promise(setImmediate);内部async测试回调。这是一个使用它来让 HTTP 请求useEffect(对于componentDidMount)解析并在运行断言之前触发重新渲染的工作示例

组件 ( LatestGist.js):

import axios from "axios";
import React, {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>}
    </>
  );
};

测试 ( 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 "../src/components/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"]')
      .hostNodes()
    ;
    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"]')
      .hostNodes()
    ;
    expect(gist.exists()).toBe(true);
    expect(gist.text()).toContain("test name");
    expect(gist.text()).toContain("some date");
  });
});

使用类似的行强制失败的断言expect(gist.text()).toContain("foobar");不会导致套件崩溃:

● 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)

这是我的依赖项:

{
  "dependencies": {
    "axios": "^0.18.0",
    "react": "^16.8.6",
    "react-dom": "^16.8.6"
  },
  "devDependencies": {
    "enzyme": "3.9.0",
    "enzyme-adapter-react-16": "1.12.1",
    "jest": "24.7.1",
    "jest-environment-jsdom": "24.7.1"
  }
}