我刚刚开始学习 Daniel Irvine 的《Mastering React Test-Driven Development》,我认为将示例转换为 React 18 应该不会太难。但是我在转换书中的第一个测试时遇到了麻烦使用 Jest。
这本书没有使用
create-react-app
或任何东西,而是从头开始构建React应用程序,所以我很难找到如何转换代码的相关示例。
当按照书中的方式以 React 17 风格编写时,测试通过。但如果我将
ReactDOM.render()
替换为 createRoot()
,测试就会失败。
我的应用程序目录如下所示:
├── package.json
├── package-lock.json
├── src
│ └── Appointment.js
└── test
└── Appointment.test.js
文件内容为:
package.json
:
{
"name": "appointments",
"version": "1.0.0",
"description": "Appointments project from Mastering React Test-Driven Development.",
"main": "index.js",
"scripts": {
"test": "jest"
},
"repository": {
"type": "git",
"url": "example.com"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/plugin-transform-runtime": "^7.18.6",
"@babel/preset-env": "^7.18.6",
"@babel/preset-react": "^7.18.6",
"jest": "^28.1.2",
"jest-environment-jsdom": "^28.1.3"
},
"dependencies": {
"@babel/runtime": "^7.18.6",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"jest": {
"testEnvironment": "jsdom"
}
}
src/Appointment.js
:
import React from 'react';
export const Appointment = () => <div>Ashley</div>;
test/Appointment.test.js
:
import React from 'react';
import ReactDOM from 'react-dom';
// import {createRoot} from 'react-dom/client';
import {Appointment} from '../src/Appointment';
describe('Appointment', () => {
it("renders the customer's first name.", () => {
const customer = {firstName: 'Ashley'};
const component = <Appointment customer={customer} />;
const container = document.createElement('div');
document.body.appendChild(container);
ReactDOM.render(component, container);
// const root = createRoot(container);
// root.render(component);
expect(document.body.textContent).toMatch('Ashley');
});
});
使用
ReactDOM.render()
,测试通过,但出现以下错误:
console.error
Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot
11 | document.body.appendChild(container);
12 |
> 13 | ReactDOM.render(component, container);
| ^
14 |
15 | expect(document.body.textContent).toMatch('Ashley');
16 | });
at printWarning (node_modules/react-dom/cjs/react-dom.development.js:86:30)
at error (node_modules/react-dom/cjs/react-dom.development.js:60:7)
at Object.render (node_modules/react-dom/cjs/react-dom.development.js:29670:5)
at Object.render (test/Appointment.test.js:13:14)
我查找了如何将
ReactDOM.render()
转换为createRoot()
,并将测试更改为:
import React from 'react';
// import ReactDOM from 'react-dom';
import {createRoot} from 'react-dom/client';
import {Appointment} from '../src/Appointment';
describe('Appointment', () => {
it("renders the customer's first name.", () => {
const customer = {firstName: 'Ashley'};
const component = <Appointment customer={customer} />;
const container = document.createElement('div');
document.body.appendChild(container);
// ReactDOM.render(component, container);
const root = createRoot(container);
root.render(component);
expect(document.body.textContent).toMatch('Ashley');
});
});
测试失败如下:
> [email protected] test
> jest
FAIL test/Appointment.test.js
Appointment
✕ renders the customer's first name. (9 ms)
● Appointment › renders the customer's first name.
expect(received).toMatch(expected)
Expected substring: "Ashley"
Received string: ""
17 | root.render(component);
18 |
> 19 | expect(document.body.textContent).toMatch('Ashley');
| ^
20 | });
21 | });
22 |
at Object.toMatch (test/Appointment.test.js:19:39)
at TestScheduler.scheduleTests (node_modules/@jest/core/build/TestScheduler.js:317:13)
at runJest (node_modules/@jest/core/build/runJest.js:407:19)
at _run10000 (node_modules/@jest/core/build/cli/index.js:339:7)
at runCLI (node_modules/@jest/core/build/cli/index.js:190:3)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 0 total
Time: 0.979 s, estimated 1 s
Ran all test suites.
如何使用
createRoot()
通过此测试?
经过更多挖掘后,我发现
act()
可以在 React 18 中使用,强制在检查测试断言之前进行渲染。这允许测试立即运行,而无需在测试失败时等待 Jest 的 did() 超时。
在 Jest 配置中,globals.IS_REACT_ACT_ENVIRONMENT
必须设置为 true
。我在这里更新了package.json
:
package.json
:
...
"jest": {
"testEnvironment": "jsdom",
"globals": {
"IS_REACT_ACT_ENVIRONMENT": true
}
}
...
然后可以更新测试以使用
act()
中的 react-dom/test-utils
:
import React from 'react';
import {createRoot} from 'react-dom/client';
import {act} from 'react-dom/test-utils';
import {Appointment} from '../src/Appointment';
describe('Appointment', () => {
it("renders the customer's first name.", () => {
const customer = {firstName: 'Ashley'};
const component = <Appointment customer={customer} />;
const container = document.createElement('div');
document.body.appendChild(container);
const root = createRoot(container);
act(() => root.render(component));
expect(document.body.textContent).toMatch('Ashley');
});
});
资源:
React 17 看起来会在您调用时立即渲染
ReactDOM.render
:
const App = () => {
return 'foo';
};
ReactDOM.render(<App />, document.querySelector('.react'));
console.log(document.querySelector('.react').textContent);
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<div class='react'></div>
相比之下,React 18 则不然,它仅在任何其他代码完成后才执行渲染工作(通常会在几毫秒后):
const App = () => {
return 'foo';
};
ReactDOM.createRoot(document.querySelector('.react')).render(<App />);
console.log(document.querySelector('.react').textContent.trim());
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div class='react'></div>
这种差异肉眼无法察觉,但会导致测试失败,因为在
expect
运行时渲染尚未发生。
一种选择是使用测试的回调形式,以便您可以在渲染发生后调用
expect
。
it("renders the customer's first name.", (done) => {
const component = <Appointment customer={ { firstName: 'Ashley' } } />;
const container = document.body.appendChild(document.createElement('div'));
createRoot(container).render(component);
setTimeout(() => {
expect(document.body.textContent).toMatch('Ashley');
done();
});
});
const App = () => {
return 'foo';
};
ReactDOM.createRoot(document.querySelector('.react')).render(<App />);
setTimeout(() => {
console.log(document.querySelector('.react').textContent.trim());
});
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div class='react'></div>