调试与测试
调试
除了使用浏览器的调试工具来完成开发中的调试外,Umi 还推荐以下调试方式来协助项目的调试。
调试 dev 产物
如果你需要在 dev 阶段调试项目的构建产物,以 umi.js 举例。先将原来的 umi.js 下载到当前项目根目录下。根据调试需要进行编辑后,刷新浏览器,项目使用的 umi.js 就替换成了根目录下的 umi.js 文件。调试完毕需要恢复就直接删除根目录的 umi.js 即可
举例:
# 下载当前项目的 umi.js
$curl http://127.0.0.1:8000/umi.js -O
# 增加想调试的内容,举例增加 "debug!!!" 弹窗
$ echo -e '\n;alert("debug!!!");\n' >> umi.js
# 打开浏览器就能看到 alert 弹窗
# 退出调试,恢复到正常状态
$rm umi.js以此类推即可调试其他的 JavaScript 文件
XSwitch
如果需要在特定的域名环境调试或者验证当前的修改的代码,推荐使用 Chrome 插件 XSwitch (Make it easy to forward request urls)
假设我们想在线上项目地址 https://www.myproject.com 上调试本地代码。项目使用 https://www.myproject.com/umi.hash.js,为了验证本地的项目,需要将它替换成本地开发环境的 http://127.0.0.1:000/umi.js
首先使用环境变量 SOCKET_SERVER 启动本地环境(防止因为连接不上 socket server 导致页面不断刷新)
$SOCKET_SERVER=http://127.0.0.1:8000/ npx umi dev然后,在 XSwitch 中配置资源转发规则。
{
"proxy": [
// 数组的第 0 项的资源会被第 1 项目替换
[
"https://www.myproject.com/umi.2c8a01df.js",
"http://127.0.0.1:8000/umi.js"
],
// 使用正则可以方便处理分包情况下 js 资源的加载
[
"https://www.myproject.com/(.*\.js)",
"http://127.0.0.1:8000/$1",
],
// 如果需要验证视觉表现,不要忘记替换 css 资源
[
"https://www.myproject.com/umi.ae8b10e0.css",
"http://127.0.0.1:8000/umi.css"
]
]
}刷新页面,正式域名下的内容就被替换了,这个时候就能方便的指定环境下调试了
如果要退出调试,关闭 XSwitch 插件功能即可

经常使用 XSwitch 的话,可新建一个规则保存。

测试
自动化测试是保障质量的有效手段,Umi 4 提供单元测试的脚手架。Umi 4 推荐使用 Jest 和 @testing-library/react 来完成项目中的单元测试
配置
使用 Umi 4 的微生成器快速的配置好 Jest, 如果你需要修改 jest 相关的配置,可以在 jest.config.ts 修改
umi 项目
import { Config, configUmiAlias, createConfig } from 'umi/test';
export default async () => {
return (await configUmiAlias({
...createConfig({
target: 'browser',
jsTransformer: 'esbuild',
jsTransformerOpts: { jsx: 'automatic' },
}),
// 覆盖 umi 的默认 jest 配置, 如
// displayName: "Umi jest",
})) as Config.InitialOptions;
};@umijs/max 项目
import { Config, configUmiAlias, createConfig } from '@umijs/max/test';
export default async () => {
return (await configUmiAlias({
...createConfig({
target: 'browser',
jsTransformer: 'esbuild',
jsTransformerOpts: { jsx: 'automatic' },
}),
// 覆盖 umi 的默认 jest 配置, 如
// displayName: "Umi jest",
})) as Config.InitialOptions;
};配置完后,就可以开始编写单元测试了
与 UI 无关的测试
假设我们需要测试一个 utils 函数 reverseApiData, 它将 api 请求的结果 data 对象的 key 和 value 互换
推荐将测试文件被测模块放在同一级目录,这样可以方便查看测试文件以便理解模块的功能
.
└── utils
├── reverseApiData.test.tss
└── reverseApiData.ts// utils/reverseApiData.ts
export async function reverseApiData(url: string, fetcher = fetch) {
const res = await fetcher(url);
const json = await res.json();
const { data = {} } = json;
const reversed: Record<string, any> = {};
for (const key of Object.keys(data)) {
const val = data[key];
reversed[val] = key;
}
return reversed;
}先来写我们第一个测试用例, 确保 fetcher 使用传入的 url 请求 api 的数据
import { reverseApiData } from './reverseApiData';
// 测试用例名字表明测试的目的
test('reverseApiData use fetcher to request url', async () => {
// 测试用例以 3A 的结构来写
// Arrange 准备阶段,准备 mock 函数或者数据
const fetcher = jest.fn().mockResolvedValue({
json: () => Promise.resolve(),
});
// Act 执行被测对象
await reverseApiData('https://api.end/point', fetcher);
// Assert 断言测试结果
expect(fetcher).toBeCalledWith('https://api.end/point');
});执行测试
$npx jest
info - generate files
PASS src/utils/reverseApiData.test.ts
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.894 s, estimated 1 s
Ran all test suites.可以使用
npx jest --watch让 jest 进程不退出,这样能省去启动重新 jest 的等待时间。
我们再写一个用例来测试这个工具函数完成了键值的对换功能
test('reverseApiData reverse simple object', async () => {
const fetcher = jest.fn().mockResolvedValue({
json: () => Promise.resolve({ data: { a: 'b' } }),
});
const reversed = await reverseApiData('url', fetcher);
expect(reversed).toEqual({ b: 'a' });
});让每个测试用例只关注一个功能点,可以让用例在重构的时候给我们更准确的反馈,改动破坏了什么功能。更多的用例请 参考
UI 测试
组件和 UI 相关的测试推荐使用 @testing-library/react。
渲染结果判断
- 使用 jest 的 snapshot
// examples/test-test/components/Greet/Greet.test.tsx
import { render } from '@testing-library/react';
import React from 'react';
import Greet from './Greet';
test('renders Greet without name by snapshot', () => {
const { container } = render(<Greet />);
expect(container).toMatchSnapshot();
});执行 npx jest 后会在测试用例同级目录会生成 __snapshots__ 文件夹和用例的 snapshot,请加入到版本管理中
- 使用 jest 的 inline snapshot
// examples/test-test/components/Greet/Greet.test.tsx
test('renders Greet without name by inline snapshot', () => {
const { container } = render(<Greet />);
expect(container).toMatchInlineSnapshot();
});执行 npx jest 后会在 toMatchInlineSnapshot 函数的参数中填入 snapshot 字符串;这种方式适合渲染结果比较短的内容
- 使用 @testing-library/jest-dom 断言
// examples/test-test/components/Greet/Greet.test.tsx
import '@testing-library/jest-dom';
test('renders Greet without name assert by testing-library', () => {
const { container } = render(<Greet />);
const greetDom = screen.getByText('Anonymous');
expect(greetDom).toBeInTheDocument();
});更多断言 API
组件行为判断
// examples/test-test/components/Greet/Greet.test.tsx
test('Greet click', async () => {
const onClick = jest.fn();
const { container } = render(<Greet onClick={onClick} />);
const greetDom = screen.getByText('Anonymous');
await fireEvent.click(screen.getByText(/hello/i));
expect(onClick).toBeCalledTimes(1);
});