我正在使用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
答案 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 提供 setImmediate
或 process.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)
开玩笑,你总是可以嘲笑。所以你需要的是:
jest.mock('React', () => ({
...jest.requireActual('React'),
useEffect: jest.fn(),
}));
这允许只模拟 useEffect 并保持其他实现实际。
import { useEffect } from 'react';
useEffect.mock.calls[0](); // <<-- That will call implementation of your useEffect