在 Jest 测试中触发 useEffect

Bat*_*man 13 javascript reactjs jestjs enzyme

我正在使用 Jest 和 Enzyme 来测试 React 功能组件。

我的组件:

export const getGroups = async () => {
    const data = await fetch(groupApiUrl);
    return await data.json()
};

export default function MyWidget({
  groupId,
}) {
  // Store group object in state
  const [group, setGroup] = useState(null);

  // Retrive groups on load
  useEffect(() => {
    if (groupId && group === null) {
      const runEffect = async () => {
        const  { groups  } = await getGroups();
        const groupData = groups.find(
          g => g.name === groupId || g.id === Number(groupId)
        );

        setGroup(groupData);
      };
      runEffect();
    }
  }, [group, groupId]);

  const params =
    group && `&id=${group.id}&name=${group.name}`;
  const src = `https://mylink.com?${params ? params : ''}`;

  return (
    <iframe src={src}></iframe>
  );
}
Run Code Online (Sandbox Code Playgroud)

当我写这个测试时:

  it('handles groupId and api call ', () => {
    // the effect will get called
    // the effect will call getGroups
    // the iframe will contain group parameters for the given groupId


   act(()=> {
        const wrapper = shallow(<MyWidget surface={`${USAGE_SURFACES.metrics}`} groupId={1} />) 
        console.log(wrapper.find("iframe").prop('src'))
    })
   })
Run Code Online (Sandbox Code Playgroud)

返回的 src 不包含 url 中的组信息。我如何触发 useEffect 以及其中的所有内容?

编辑:我学到的一件事是shallow不会触发useEffect。我仍然没有得到正确的 src 但我已经切换到mount而不是shallow

ggo*_*len 5

这是一个最小的、完整的 mocking 示例fetch。您的组件几乎可以归结为通用的 fire-fetch-and-set-state-with-response-data 习惯用法:

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

export default function Users() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    (async () => {
      const res = await fetch("https://jsonplaceholder.typicode.com/users");
      setUsers(await res.json());
    })();
  }, []);

  return <p>there are {users.length} users</p>;
};
Run Code Online (Sandbox Code Playgroud)

随意在浏览器中运行这个组件:

<script type="text/babel" defer>
const {useState, useEffect} = React;

const Users = () => {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    (async () => {
      const res = await fetch("https://jsonplaceholder.typicode.com/users");
      setUsers(await res.json());
    })();
  }, []);

  return <p>there are {users.length} users</p>;
};

ReactDOM.render(<Users />, document.body);
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
Run Code Online (Sandbox Code Playgroud)

您可以看到组件最初呈现的值为 0,然后当请求到达时,所有 10 个用户对象都处于状态,并触发第二次呈现以显示更新的文本。

让我们编写一个幼稚(但不正确)的单元测试,嘲笑fetch它,因为它在 Node 中不存在:

import {act} from "react-dom/test-utils";
import React from "react";
import Enzyme, {mount} from "enzyme";
import Adapter from "enzyme-adapter-react-16";

import Users from "../src/Users";

Enzyme.configure({adapter: new Adapter()});

describe("Users", () => {
  let wrapper;
  let users;
  
  beforeEach(() => {
    const mockResponseData = [{id: 1}, {id: 2}, {id: 3}];
    users = mockResponseData.map(e => ({...e}));
    jest.clearAllMocks();
    global.fetch = jest.fn(async () => ({
      json: async () => mockResponseData
    }));
    wrapper = mount(<Users />);
  });
  
  it("renders a count of users", () => {
    const p = wrapper.find("p");
    expect(p.exists()).toBe(true);
    expect(p.text()).toEqual("there are 3 users");
  });
});
Run Code Online (Sandbox Code Playgroud)

一切看起来都很好——我们加载包装器,找到段落并检查文本。但是运行它会给出:

import {act} from "react-dom/test-utils";
import React from "react";
import Enzyme, {mount} from "enzyme";
import Adapter from "enzyme-adapter-react-16";

import Users from "../src/Users";

Enzyme.configure({adapter: new Adapter()});

describe("Users", () => {
  let wrapper;
  let users;
  
  beforeEach(() => {
    const mockResponseData = [{id: 1}, {id: 2}, {id: 3}];
    users = mockResponseData.map(e => ({...e}));
    jest.clearAllMocks();
    global.fetch = jest.fn(async () => ({
      json: async () => mockResponseData
    }));
    wrapper = mount(<Users />);
  });
  
  it("renders a count of users", () => {
    const p = wrapper.find("p");
    expect(p.exists()).toBe(true);
    expect(p.text()).toEqual("there are 3 users");
  });
});
Run Code Online (Sandbox Code Playgroud)

显然,没有等待承诺并且包装器没有注册更改。当承诺在任务队列中等待时,断言在调用堆栈上同步运行。当承诺用数据解析时,套件已经结束。

我们想让测试块到await下一个滴答,也就是在运行之前等待调用堆栈和挂起的承诺解决。Node 提供setImmediateprocess.nextTick用于实现此目的。

最后,该wrapper.update()函数启用与 React 组件树的同步,以便我们可以看到更新的 DOM。

这是最终的工作测试:

import {act} from "react-dom/test-utils";
import React from "react";
import Enzyme, {mount} from "enzyme";
import Adapter from "enzyme-adapter-react-16";

import Users from "../src/Users";

Enzyme.configure({adapter: new Adapter()});

describe("Users", () => {
  let wrapper;
  let users;
  
  beforeEach(() => {
    const mockResponseData = [{id: 1}, {id: 2}, {id: 3}];
    users = mockResponseData.map(e => ({...e}));
    jest.clearAllMocks();
    global.fetch = jest.fn(async () => ({
      json: async () => mockResponseData
    }));
    wrapper = mount(<Users />);
  });
  
  it("renders a count of users", async () => {
    //                           ^^^^^
    await act(() => new Promise(setImmediate)); // <--
    wrapper.update();                           // <--
    const p = wrapper.find("p");
    expect(p.exists()).toBe(true);
    expect(p.text()).toEqual("there are 3 users");
  });
});
Run Code Online (Sandbox Code Playgroud)

new Promise(setImmediate)技术还帮助我们在承诺解决之前对状态进行断言。act(from react-dom/test-utils) 是必要的,以避免Warning: An update to Users inside a test was not wrapped in act(...)弹出useEffect.

将这个测试添加到上面的代码中也通过了:

it("renders a count of 0 users initially", () => {
  return act(() => {
    const p = wrapper.find("p");
    expect(p.exists()).toBe(true);
    expect(p.text()).toEqual("there are 0 users");
    return new Promise(setImmediate);
  });
});
Run Code Online (Sandbox Code Playgroud)

使用 时测试回调是异步的setImmediate,因此必须返回一个 promise 以确保 Jest 正确等待它。

这篇文章使用了 Node 12、Jest 26.1.0、Enzyme 3.11.0 和 React 16.13.1。


Max*_*Max 5

你总是可以用玩笑来嘲笑。所以你需要的是:

  1. 在你的单元测试模拟中使用 React 的 useEffect
jest.mock('React', () => ({
  ...jest.requireActual('React'),
  useEffect: jest.fn(),
}));
Run Code Online (Sandbox Code Playgroud)

这允许仅模拟 useEffect 并保持其他实现实际。

  1. 导入useEffect以在测试中使用它
import { useEffect } from 'react';
Run Code Online (Sandbox Code Playgroud)
  1. 最后在您的测试中,在渲染组件后调用模拟
useEffect.mock.calls[0](); // <<-- That will call implementation of your useEffect
Run Code Online (Sandbox Code Playgroud)

  • 这样,我收到错误属性“mock”在类型“(effect: EffectCallback, deps?: DependencyList | undefined) =&gt; void”上不存在。 (4认同)

mei*_*sam 0

useEffect 已经被触发并正在工作,重点是它是一个async操作。所以需要等待fetch完成。您可以执行此操作的方法之一是: 1. 编写断言 2. 指定测试中的断言数量,以便 jest 知道它必须等待操作完成。

 it('handles groupId and api call ', () => {
    // the effect will get called
    // the effect will call getGroups
    // the iframe will contain group parameters for the given groupId
    expect.assertions(1)
    const wrapper = shallow(<UsageWidget surface={`${USAGE_SURFACES.metrics}`} groupId={2} />)
    wrapper.update()
    expect(whatever your expectation is)
  });
Run Code Online (Sandbox Code Playgroud)

因为在这个例子中我只是写了断言,

expect.assertions(1)
Run Code Online (Sandbox Code Playgroud)

如果写多了,就需要改数字。