【问题标题】:Jest: tests can't fail within setImmediate or process.nextTick callback开玩笑:测试不能在 setImmediate 或 process.nextTick 回调中失败
【发布时间】:2017-06-07 04:05:36
【问题描述】:

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

这可以正常工作,但如果在调用 setImmediateprocess.nextTick 时测试失败,则 Jest 不会处理该异常并提前退出。下面,你可以看到我什至试图捕捉这个异常,但无济于事。

如何在 Jest 中使用 setImmediatenextTick 之类的内容?这个问题的公认答案是我试图实现的失败: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

节点 v6.9.1

【问题讨论】:

  • 您是否阅读过有关测试异步内容的文档 (facebook.github.io/jest/docs/tutorial-async.html#content)?
  • 是的,我不是在测试异步函数,而是在测试一个需要等待承诺的组件。
  • 但是你创建了一个promise,即使你在测试中解决了它也不会在测试运行中产生影响,你至少需要从测试中返回promise。
  • 但是你创建了一个promise,即使你在测试中解决了它也不会在测试运行中产生影响,你至少需要从测试中返回promise。
  • 您是否设法解决了这个问题@AlexPaterson?我遇到了同样的问题,唯一可行的方法是将期望包装到 try catch 中……对我的口味来说有点难看。这也可能是相关的; github.com/facebook/jest/issues/2059

标签: node.js reactjs jasmine jestjs


【解决方案1】:

另一个可能更清洁的解决方案,使用 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);
});

【讨论】:

    【解决方案2】:

    用 atm 的下一个方式克服这个问题(也解决了 componentDidMount 和 async setState 中 Enzyme 和异步调用的问题):

    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,Jest 21.1.6)

    更新

    刚刚想出了另一个更好(但仍然很奇怪)的解决方案,使用 async/await (它仍然解决异步 componentDidMount 和异步 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 进行更新,而有些则不需要,即使我正在为类似的事情测试相同的组件。
    【解决方案3】:

    一些注意事项;

    • 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();
        });
    });
    

    【讨论】:

    【解决方案4】:

    正如其他人所展示的那样,将传递给process.nextTicksetImmediate 的回调块包装在try/catch 中是可行的,但这既冗长又令人分心。

    更简洁的方法是在 async 测试回调中使用简短行 await new Promise(setImmediate); 刷新承诺。这是一个使用它来让useEffect(对componentDidMount 同样有用)中的 HTTP 请求在运行断言之前解析并触发重新渲染的工作示例:

    组件(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"
      }
    }
    

    【讨论】:

      【解决方案5】:

      继弗拉基米尔的回答 + 编辑之后,这里有一个对我有用的替代方案。而不是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);
      });
      

      【讨论】:

        【解决方案6】:

        -- 作为答案发布不能格式化 cmets 中的代码块。 --

        根据 Vladimir 的回答,请注意使用 async/await 也适用于 beforeEach:

        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(…)
        })
        

        【讨论】:

          猜你喜欢
          • 2020-10-08
          • 2017-08-11
          • 2018-02-13
          • 2017-11-19
          • 2017-02-11
          • 2018-07-20
          • 2021-06-26
          • 2020-08-19
          • 2021-05-02
          相关资源
          最近更新 更多