在Jest测试中触发useEffect

时间:2019-12-02 22:15:37

标签: javascript reactjs jestjs enzyme

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

MyComponent:

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

当我编写此测试时:

  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'))
    })
   })

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

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

4 个答案:

答案 0 :(得分:4)

这是一个模拟 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>;
};

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

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

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

让我们编写一个简单的(但不正确的)单元测试,mocking 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");
  });
});

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

Error: expect(received).toEqual(expected) // deep equality

Expected: "there are 3 users"
Received: "there are 0 users"

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

我们想让测试块在下一个滴答声中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");
  });
});

new Promise(setImmediate) 技术还可以帮助我们在承诺解决之前对状态进行断言。 act(来自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);
  });
});

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

本文使用 Node 12、Jest 26.1.0、Enzyme 3.11.0 和 React 16.13.1。

答案 1 :(得分:0)

useEffect已经被触发并且可以正常工作,关键是它的async操作。因此,您需要等待提取完成。您可以执行的一种方法是: 1.写下你的断言 2.指定测试中的断言数量,以使玩笑者知道它必须等待操作完成。

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

因为在这个例子中我只是在断言上写过

expect.assertions(1)

如果要写更多,则需要更改数字。

答案 2 :(得分:0)

您可以设置超时以异步检查是否满足预期条件。

import json
import base64
from google.cloud import bigquery
import ast
import pandas as pd
import sys
import pandas_gbq


def process_data(data):
    #msg = str(data)
    df = pd.DataFrame({"Data":data},index=[0])
    df['time'] = pd.datetime.now()
    lst = list(df)
    df[lst] = df[lst].astype(str)
    pandas_gbq.to_gbq(df,'datasetid.tableid',project_id='project_id',if_exists='append')


def receive_messages(project_id, subscription_name):
    """Receives messages from a pull subscription."""
    # [START pubsub_subscriber_async_pull]
    # [START pubsub_quickstart_subscriber]
    import time

    from google.cloud import pubsub_v1

    # TODO project_id = "Your Google Cloud Project ID"
    # TODO subscription_name = "Your Pub/Sub subscription name"

    subscriber = pubsub_v1.SubscriberClient()
    # The `subscription_path` method creates a fully qualified identifier
    # in the form `projects/{project_id}/subscriptions/{subscription_name}`
    subscription_path = subscriber.subscription_path(
        project_id, subscription_name)

    def callback(message):
        #print('Received message: {}'.format(message))
        process_data(message)
        message.ack()

    subscriber.subscribe(subscription_path, callback=callback)

    # The subscriber  is non-blocking. We must keep the main thread from
    # exiting to allow it to process messages asynchronously in the background.
   # print('Listening for messages on {}'.format(subscription_path))
    while True:
        time.sleep(60)
    # [END pubsub_subscriber_async_pull]
    # [END pubsub_quickstart_subscriber]


receive_messages(project-id,sub-id)

超时使您可以等待异步提取完成。最后调用it('handles groupId and api call ', (done) => { const wrapper = shallow(<UsageWidget surface={`${USAGE_SURFACES.metrics}`} groupId={1} />) setTimeout(() => { expect(wrapper.find("iframe").prop('src')).toBeTruthy(); // or whatever done(); }, 5000); } ,以表明done()块已完成。

您可能还想对it()函数进行mock implementation的操作,以免每次测试代码时都不会真正碰到网络API。

答案 3 :(得分:0)

开玩笑,你总是可以嘲笑。所以你需要的是:

  1. 在你的单元测试中模拟来自 React 的 useEffect
jest.mock('React', () => ({
  ...jest.requireActual('React'),
  useEffect: jest.fn(),
}));

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

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