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

Posted

技术标签:

【中文标题】开玩笑:测试不能在 setImmediate 或 process.nextTick 回调中失败【英文标题】:Jest: tests can't fail within setImmediate or process.nextTick callback 【发布时间】: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 【参考方案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();
    );
);

【讨论】:

不应该是return fail(e)而不是return done(e);吗? 据我所知没有fail 方法,只有done 回调:facebook.github.io/jest/docs/en/asynchronous.html#callbacks 应该是done.fail(e)。如果你这样做 done(e) 测试将通过 done() 不接受参数。 fail 函数记录在 Jasmine 页面上:done.fail function【参考方案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(…)
)

【讨论】:

以上是关于开玩笑:测试不能在 setImmediate 或 process.nextTick 回调中失败的主要内容,如果未能解决你的问题,请参考以下文章

`toBeInstanceOf(Number)` 不能开玩笑

开玩笑测试后杀死或关闭阿波罗服务器上的 websocket

不能用玩笑模拟模块,并测试函数调用

在所有测试运行后开玩笑清理

el-checkbox 不能用 jest 设置检查或至少在 v-model 上没有变化

导入图像打破了开玩笑的测试