【发布时间】:2021-07-04 08:53:52
【问题描述】:
我正在尝试弄清楚如何测试使用 useEffect 进行 API 调用以获取数据的更新状态的组件。我认为有几件事情在我能再说下去之前是很重要的,那就是我正在使用的文件/包。
首先,我有一个名为 App.tsx 的主要组件,在 App.tsx 内部,在 useEffect 内部,我对外部 API 进行 fetch 调用以获取 Queen 的歌曲数组。我还渲染了一个<Song /> 组件,使用.map 迭代每首歌曲,并使用.filter 根据文本输入在UI 上过滤歌曲。我正在使用自定义钩子。这是我为该组件及其自定义钩子提供的代码。
// App.tsx
type ISong = {
id: number;
title: string;
lyrics: string;
album: string;
};
export default function App() {
const { songs, songError } = useSongs();
const { formData, handleFilterSongs } = useForm();
return (
<Paper>
<h1>Queen Songs</h1>
<FilterSongs handleFilterSongs={handleFilterSongs} />
<section>
{songError ? (
<p>Error loading songs...</p>
) : !songs ? (
<>
<p data-testid="loadingText">Loading...</p>
<Loader />
</>
) : (
<Grid container>
{songs
.filter(
(song: ISong) =>
song.title
.toLowerCase()
.includes(formData.filter.toLowerCase()) ||
song.album
.toLowerCase()
.includes(formData.filter.toLowerCase()) ||
song.lyrics
.toLowerCase()
.split(" ")
.join(" ")
.includes(formData.filter.toLowerCase())
)
.map((song: ISong) => (
<Grid key={song.id} item>
<Song song={song} />
</Grid>
))}
</Grid>
)}
</section>
</Paper>
);
}
// useSongs.tsx
type ISongs = {
id: number;
title: string;
lyrics: string;
album: string;
}[];
type IError = {
message: string;
};
export default function useSongs() {
const [songs, setSongs] = useState<ISongs | null>(null);
const [songError, setSongError] = useState<IError | null>(null);
useEffect(() => {
fetch("https://queen-songs.herokuapp.com/songs")
.then(res => res.json())
.then(songs => setSongs(songs))
.catch(err => setSongError(err));
}, []);
return {songs, songError}
}
接下来是我的 App.test.tsx 文件。我使用react-testing-library 和jest-dom/extend-expect 进行测试覆盖。这是我的测试文件代码。我一直在观看有关此事的 youtube 教程,并阅读了很多文章,但我仍然无法弄清楚。
// App.test.tsx
import * as rctl from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import App from "./App";
// @ts-ignore
global.fetch = jest.fn(() =>
Promise.resolve({
json: () =>
Promise.resolve({
value: [{title: "title1", album: "album1", lyrics: "asdf", id: 1}, {title: "title2", album: "album2", lyrics: "zxcv", id: 2}, etc...],
}),
})
);
describe.only("The App component should", () => {
it("load songs from an API call after initial render", async () => {
await rctl.act(async () => {
await rctl.render(<App />).debug();
rctl.screen.debug();
});
});
});
这段代码给了我以下错误信息
FAIL src/pages/App/App.test.tsx
App
× loads the songs on render (117 ms)
● App › loads the songs on render
TypeError: Cannot read property 'then' of undefined
17 |
18 | useEffect(() => {
> 19 | fetch("https://queen-songs.herokuapp.com/songs")
| ^
20 | .then(res => res.json())
21 | .then(songs => {
22 | setSongs(songs)
at src/pages/App/useSongs.ts:19:7
at invokePassiveEffectCreate (node_modules/react-dom/cjs/react-dom.development.js:23487:20)
at HTMLUnknownElement.callCallback (node_modules/react-dom/cjs/react-dom.development.js:3945:14)
at HTMLUnknownElement.callTheUserObjectsOperation (node_modules/jsdom/lib/jsdom/living/generated/EventListener.js:26:30)
at innerInvokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:338:25)
at invokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:274:3)
at HTMLUnknownElementImpl._dispatch (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:221:9)
at HTMLUnknownElementImpl.dispatchEvent (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:94:17)
at HTMLUnknownElement.dispatchEvent (node_modules/jsdom/lib/jsdom/living/generated/EventTarget.js:231:34)
at Object.invokeGuardedCallbackDev (node_modules/react-dom/cjs/react-dom.development.js:3994:16)
at invokeGuardedCallback (node_modules/react-dom/cjs/react-dom.development.js:4056:31)
at flushPassiveEffectsImpl (node_modules/react-dom/cjs/react-dom.development.js:23574:9)
at unstable_runWithPriority (node_modules/scheduler/cjs/scheduler.development.js:468:12)
at runWithPriority$1 (node_modules/react-dom/cjs/react-dom.development.js:11276:10)
at flushPassiveEffects (node_modules/react-dom/cjs/react-dom.development.js:23447:14)
at Object.<anonymous>.flushWork (node_modules/react-dom/cjs/react-dom-test-utils.development.js:992:10)
at flushWorkAndMicroTasks (node_modules/react-dom/cjs/react-dom-test-utils.development.js:1001:5)
at node_modules/react-dom/cjs/react-dom-test-utils.development.js:1080:11
console.log
<body>
<div>
<div
class="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
style="text-align: center; overflow: hidden; min-height: 100vh;"
>
<h1>
Queen Songs
</h1>
<div
style="display: flex; flex-flow: column; justify-content: center; text-align: center;"
>
<input
data-testid="input"
id="filter"
name="filter"
placeholder="Search by title, album name, or lyrics here..."
style="width: 18.75rem; height: 1.875rem; align-self: center; text-align: center; font-style: italic;"
type="text"
/>
</div>
<section
style="display: flex; flex-flow: row wrap; justify-content: center; align-items: center;"
>
<p
data-testid="loadingText"
>
Loading...
</p>
<div
class="line-container"
>
<div
class="line"
data-testid="loader-line"
/>
</div>
</section>
</div>
</div>
</body>
at Object.debug (node_modules/@testing-library/react/dist/pure.js:107:13)
console.log
<body>
<div>
<div
class="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
style="text-align: center; overflow: hidden; min-height: 100vh;"
>
<h1>
Queen Songs
</h1>
<div
style="display: flex; flex-flow: column; justify-content: center; text-align: center;"
>
<input
data-testid="input"
id="filter"
name="filter"
placeholder="Search by title, album name, or lyrics here..."
style="width: 18.75rem; height: 1.875rem; align-self: center; text-align: center; font-style: italic;"
type="text"
/>
</div>
<section
style="display: flex; flex-flow: row wrap; justify-content: center; align-items: center;"
>
<p
data-testid="loadingText"
>
Loading...
</p>
<div
class="line-container"
>
<div
class="line"
data-testid="loader-line"
/>
</div>
</section>
</div>
</div>
</body>
at logDOM (node_modules/@testing-library/dom/dist/pretty-dom.js:82:13)
console.error
Error: Uncaught [TypeError: Cannot read property 'then' of undefined]
at reportException (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\helpers\runtime-script-errors.js:62:24)
at innerInvokeEventListeners (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:341:9)
at invokeEventListeners (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:274:3)
at HTMLUnknownElementImpl._dispatch (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:221:9)
at HTMLUnknownElementImpl.dispatchEvent (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:94:17)
at HTMLUnknownElement.dispatchEvent (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\generated\EventTarget.js:231:34)
at Object.invokeGuardedCallbackDev (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom.development.js:3994:16)
at invokeGuardedCallback (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom.development.js:4056:31)
at flushPassiveEffectsImpl (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom.development.js:23574:9)
at unstable_runWithPriority (C:\Users\brian\Code\cra-queen-api-fe\node_modules\scheduler\cjs\scheduler.development.js:468:12) TypeError: Cannot read property 'then' of undefined
at C:\Users\brian\Code\cra-queen-api-fe\src\pages\App\useSongs.ts:19:7
at invokePassiveEffectCreate (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom.development.js:23487:20)
at HTMLUnknownElement.callCallback (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom.development.js:3945:14)
at HTMLUnknownElement.callTheUserObjectsOperation (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\generated\EventListener.js:26:30)
at innerInvokeEventListeners (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:338:25)
at invokeEventListeners (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:274:3)
at HTMLUnknownElementImpl._dispatch (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:221:9)
at HTMLUnknownElementImpl.dispatchEvent (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:94:17)
at HTMLUnknownElement.dispatchEvent (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\generated\EventTarget.js:231:34)
at Object.invokeGuardedCallbackDev (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom.development.js:3994:16)
at invokeGuardedCallback (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom.development.js:4056:31)
at flushPassiveEffectsImpl (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom.development.js:23574:9)
at unstable_runWithPriority (C:\Users\brian\Code\cra-queen-api-fe\node_modules\scheduler\cjs\scheduler.development.js:468:12)
at runWithPriority$1 (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom.development.js:11276:10)
at flushPassiveEffects (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom.development.js:23447:14)
at Object.<anonymous>.flushWork (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom-test-utils.development.js:992:10)
at flushWorkAndMicroTasks (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom-test-utils.development.js:1001:5)
at C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom-test-utils.development.js:1080:11
at processTicksAndRejections (node:internal/process/task_queues:94:5)
at VirtualConsole.<anonymous> (node_modules/jsdom/lib/jsdom/virtual-console.js:29:45)
at reportException (node_modules/jsdom/lib/jsdom/living/helpers/runtime-script-errors.js:66:28)
at innerInvokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:341:9)
at invokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:274:3)
at HTMLUnknownElementImpl._dispatch (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:221:9)
at HTMLUnknownElementImpl.dispatchEvent (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:94:17)
console.error
The above error occurred in the <App> component:
at App (C:\Users\brian\Code\cra-queen-api-fe\src\pages\App\App.tsx:18:32)
Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.
at logCapturedError (node_modules/react-dom/cjs/react-dom.development.js:20085:23)
at update.callback (node_modules/react-dom/cjs/react-dom.development.js:20118:5)
at callCallback (node_modules/react-dom/cjs/react-dom.development.js:12318:12)
at commitUpdateQueue (node_modules/react-dom/cjs/react-dom.development.js:12339:9)
at commitLifeCycles (node_modules/react-dom/cjs/react-dom.development.js:20736:11)
at commitLayoutEffects (node_modules/react-dom/cjs/react-dom.development.js:23426:7)
at HTMLUnknownElement.callCallback (node_modules/react-dom/cjs/react-dom.development.js:3945:14)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 0 total
Time: 2.59 s, estimated 3 s
Ran all test suites related to changed files.
老实说,我在这里完全迷失了,我不知道下一步该做什么。我通常的解决问题的能力没有帮助,所以我想我会求助于 SO 寻求帮助。感谢您阅读所有这些内容以及您可能提供的任何帮助。
编辑:我剥离了 sn-ps 中大部分 CSS 的代码,使其更具可读性,这就是为什么 screen.debug() 日志包含一些 CSS 而代码不包含的原因。
编辑:我将 useEffect 方法更改为使用 async/await,现在我的测试工作正常,但我仍然有与以前相同的输出。这是更新后的useEffect 和代码输出。
// Updated useSongs.tsx
export default function useSongs() {
const [songs, setSongs] = useState<ISongs | null>(null);
const [songError, setSongError] = useState<IError | null>(null);
useEffect(() => {
(async() => {
try {
const fetchSongs = await fetch("https://queen-songs.herokuapp.com/songs");
const data = await fetchSongs.json();
setSongs(data);
} catch (error) {
setSongError(error);
}
})()
}, []);
return {songs, songError}
}
// Updated testOutput
PASS src/pages/App/App.test.tsx
App
√ loads the songs on render (52 ms)
console.log
<body>
<div>
<div
class="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
style="text-align: center; overflow: hidden; min-height: 100vh;"
>
<h1>
Queen Songs
</h1>
<section
style="display: flex; flex-flow: row wrap; justify-content: center; align-items: center;"
>
<p
data-testid="loadingText"
>
Loading...
</p>
<div
class="line-container"
>
<div
class="line"
data-testid="loader-line"
/>
</div>
</section>
</div>
</div>
</body>
at logDOM (node_modules/@testing-library/dom/dist/pretty-dom.js:82:13)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.878 s, estimated 1 s
Ran all test suites related to changed files.
我希望测试显示 HTML 在 useEffect 运行并且状态更新后,加载文本应该消失了。
【问题讨论】:
-
当你将 then(res => res.json()).then(...) 合并为一个 'then' 时会发生什么?似乎是如果 fetch 失败,第二个 then 会在 undefined object 上调用
-
@MichaelRovinsky 我将 useEffect 函数更新为 async/await(我在上面发布了新代码),但在状态更新后代码未显示更新后的 DOM。它只是在状态更新之前显示。
-
添加 useEffect(() => console.log(songs), [songs]) 到你的组件,看看歌曲是否真的更新了
-
显然没有更新。我添加了一个console.log(数据,歌曲),它注销了数据数组,但歌曲设置为空。有趣的是,该应用实际上会在进行映射和过滤的用户界面上进行更新。
-
我不明白您为什么需要自定义挂钩。把你所有的 useState/useEffect 放到组件本身,看看有没有帮助...
标签: reactjs asynchronous testing jestjs