本节内容导图
- react的路由
- route测试
- location测试
- params测试
- 根路由测试
- useRoutes方法
- matchRoutes方法
- 按照config运行
react的路由
路由主要是匹配路径,接着就是做测试,以及匹配路由过程中的一些注意事项。可以理解为路由测试是建立在组件测试之上的一个测试,因为路由测试可以包括组件那些测试,同样,我们也是使用案例来一一讲解。
route测试
根据react-router官方给出的解答,我们需要使用到MemoryRouter,它不依赖于外部源:
https://reactrouter.com/en/6.8.2/router-components/memory-router
创建项目之前我们建立了三个路由,前面已经用了home和detail,这次我们用profile。
我们新建测试文件 __tests__/react/route/route.test.tsx
import Profile from "src/page/profile/profile";
import { MemoryRouter,Routes,Route } from "react-router-dom";
import { screen,render } from "@testing-library/react";
describe('test router',() => {
// 如果需要去掉警告,可以加上这个
// let consoleWarn: jest.SpyInstance;
// beforeEach(() => {
// // eslint-disable-next-line @typescript-eslint/no-empty-function
// consoleWarn = jest.spyOn(console, "warn").mockImplementation(() => {});
// });
// afterEach(() => {
// consoleWarn.mockRestore();
// });
it('test route',() => {
const Com = (
<MemoryRouter basename="/profile" initialEntries={['/profile']}>
<Routes>
<Route path="/" element={<Profile />}></Route>
// <Route path="/" element={<div>profile123</div>}></Route>
</Routes>
</MemoryRouter>
)
const container = render(Com);
expect(container).toMatchSnapshot('route-test')
})
})
解释:
- 我们使用MemoryRouter去包裹路由Route,他有一个匹配路径initialEntries和一个基本路径basename,需要注意了,页面实际访问的路径为(path),但是这个path需要符合你定义的路由规范,即需要路由匹配,规则为:path = initialEntries - basename
- 当路由不匹配的时候,测试仍旧会通过,但是会出现一个警告:提示我们路由匹配失败,element渲染为空。此时如果想去掉警告,那么就mock掉console的warn,让他们方位空即可.
- 路由不会重新匹配,即,如果我们后面又重新匹配相同的路由/profile: profile123}>此时渲染的还是组件,而不是profile123
location测试
我们访问profile页面的时候,默认打印一次location的值为:
{
pathname: '/profile',
search: '',
hash: '',
state: null,
key: 'default'
}
咱们新建一个测试i文件 __tests__/react/route/route-location.test.tsx
import Profile from "src/page/profile/profile";
import { MemoryRouter,Routes,Route } from "react-router-dom";
import { screen,render } from "@testing-library/react";
describe('test router',() => {
it('test route',() => {
const location = {
pathname: "/home",
search: "",
hash: "",
state: null,
key: "r9qntrej",
};
const Com = (
<MemoryRouter initialEntries={['/profile/child1/123']}>
<Routes location={location}>
<Route path="/profile/child1/:id" element={<Profile />}></Route>
</Routes>
</MemoryRouter>
)
const container = render(Com);
expect(container).toMatchSnapshot('route-test')
})
})
在这里,需要注意:location优先级高于initialEntries,也就是说,虽然path匹配了路径/profile/child1/:id,但实际匹配路径是location中的pathname,而pathname=home,这个路径在initialEntries中找不到,因此匹配失败;
const Com = (
<MemoryRouter initialEntries={['/profile/child1/123']}>
<Routes>
<Route path="/profile/child1/:id" element={<ProfileChild1 />}></Route>
<Route path="/profile/child2" element={<ProfileChild2 />}></Route>
</Routes>
</MemoryRouter>
)
另一个是这种情况,initialEntries指定了一个路由,而Routes里面却有两个需要匹配的路由,此时测试可以通过,但是引入的/profile/child2路径无法渲染,同时还导致覆盖率降低了
因此,最好一个initialEntries对应一个路由,别想其他的就好。
const Com = (
<MemoryRouter basename="/profile" initialEntries={['/profile/child1/1234?username=jack&age=12']}>
<Routes>
<Route path="/child1/:id" element={<ProfileChild1 />}></Route>
</Routes>
</MemoryRouter>
)
我们只需要正常的写search即可,这样我们就能够在页面中通过useLocation拿到对应的值。同理,如果我们想传state,也是一样,但是我们需要将location传给Routes
it('test location',() => {
const location = {
pathname: "/profile/child1/1234",
search: "",
hash: "",
state: {
formDD: {
username: 'jack',
age: 23
}
},
key: "r9qntrej",
};
const Com = (
<MemoryRouter initialEntries={['/profile/child1/123']}>
<Routes location={location}>
<Route path="/profile/child1/:id" element={<ProfileChild1 />}></Route>
</Routes>
</MemoryRouter>
)
const container = render(Com);
expect(container).toMatchSnapshot('route-test')
})
因此,请记住:对于某些页面他们是有search值或者state值的,我们就可以使用这种方法去测试
params测试
测试之前先把我们的根路由加点东西
我们给profile加一个child
{
path: "/profile",
element: <Outlet />,
children: [
{
path: '',
element: <Profile />
},
{
path: 'child1/:id',
element: <ProfileChild1 />
},
{
path: 'child2',
element: <ProfileChild2 />
}
]
},
同时新建这两个页面,路径是这样子的
在页面 page/profile/Com/p-child1.tsx
import { useParams } from 'react-router-dom';
export default function ProfileChild1() {
const params = useParams();
console.log(params,'params')
return (
<div>
<p>profile child1</p>
<p>params: { params.id }</p>
</div>
)
}
然后是,测试文件 __tests__/react/route/route-params.test.tsx
import Profile from "src/page/profile/profile";
import ProfileChild1 from "src/page/profile/child/child1/p-child1";
import ProfileChild2 from "src/page/profile/child/child2/p-child2";
import { MemoryRouter,Routes,Route } from "react-router-dom";
import { screen,render } from "@testing-library/react";
describe('test params',() => {
it('test route',() => {
const Com = (
<MemoryRouter basename="/profile" initialEntries={['/profile/child1/1234']}>
<Routes>
<Route path="/child1/:id" element={<ProfileChild1 />}></Route>
</Routes>
</MemoryRouter>
)
const container = render(Com);
expect(container).toMatchSnapshot('route-params-test')
})
})
快照图
根路由测试
根路由测试,即测试router.tsx
useRoutes方法
我们一个项目至少有一个router文件夹,并导出一个根路由,最终会挂在到app上,那么这个根路由怎么测试呢?这里我用的是useRoutes创建的根路由,因此也是用它来测试路由
我们新建文件 __tests/react/route-routes.test.tsx
import { routes } from "src/router";
import * as TestRenderer from "react-test-renderer";
import type { RouteObject } from "react-router";
import { MemoryRouter,useRoutes } from "react-router-dom";
describe('test router',() => {
it('test routes',() => {
const renderer: TestRenderer.ReactTestRenderer = (
TestRenderer.create(
<MemoryRouter>
<RoutesRenderer routes={routes} />
</MemoryRouter>
)
)
expect(renderer).toMatchSnapshot()
})
})
function RoutesRenderer({
routes,
location,
}: {
routes: RouteObject[];
location?: Partial<Location> & { pathname: string };
}) {
return useRoutes(routes, location);
}
matchRoutes方法
matchRoutes针对给定的一组路由运行路由匹配算法,location以查看哪些路由(如果有)匹配。如果找到匹配项,RouteMatch则会返回一组对象,每个匹配的路由对应一个对象路由的测试,会发现我们只需要用到了他,并且测试通过了就能覆盖到。
他的源码为:
export declare function matchRoutes<
RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject
>(
routes: RouteObjectType[],
locationArg: Partial<Location> | string,
basename?: string
): AgnosticRouteMatch<string, RouteObjectType>[] | null;
可以看到它传入三个参数,一个是路由对象,另一个是匹配路径,以及一个基本路径。返回的是一个关于路由路径的字符串类型数组
import { routes } from "src/router";
import * as TestRenderer from "react-test-renderer";
import type { RouteObject } from "react-router";
import { MemoryRouter,useRoutes,matchRoutes } from "react-router-dom";
function pickPaths(routes: RouteObject[], pathname: string): string[] | null {
const matches = matchRoutes(routes, pathname);
return matches && matches.map((match) => match.route.path || "");
}
describe('test router',() => {
it('test routes',() => {
const res = pickPaths(routes, "/profile");
console.log(res,'--=-=-')
})
})
两种方法都可以,按照个人喜好使用。
按照config运行
前面组件的测试,以及现在的路由测试我们新建了很多文件,目前这些文件都保存在tests/react/**下面,那么我们总不能每一次都只运行一个文件,或者直接jest全部运行,能不能只运行部分文件?答:可以,以配置的方式运行,这里我们把配置文件已经改成了ts文件类型。
目的:运行react文件夹下面的测试文件
新建文件 __tests__/react/config/test.config.ts
// 配置类型
import type { Config } from '@jest/types'
// 导入默认的配置
import jestConfig from '../../../../jest.config';
export default {
// 对象合并
...jestConfig,
// 根路径
rootDir: '../../../..',
// 收集的文件类型,这里收集了react下面的全部文件,以及page下面的全部文件。这样在测试成功之后,如果我们加上了覆盖率检测,那么这些文件都会覆盖到
collectCoverageFrom: [
...(jestConfig.collectCoverageFrom as Array<string>),
'!**/*.(ts|tsx)',
'**/__tests__/react/*.test.tsx',
'**/page/*.tsx',
'**/page/**/*.tsx'
],
// 测试文件,这里测试的是__tests__/react下面的全部测试文件
testMatch: [
'**/src/__tests__/react/*.test.tsx',
'**/src/__tests__/react/**/*.test.tsx'
]
} as Config.InitialOptions
默认的配置我也改动了一点点,这里贴一下
import type { Config } from '@jest/types'
export default {
// 在每次测试之前自动清除模拟
clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,
// The directory where Jest should output its coverage files
coverageDirectory: "coverage",
// Indicates which provider should be used to instrument code for coverage
coverageProvider: "v8",
roots: [
"<rootDir>/src/"
],
// 这里的将一些不需要的文件剔除掉
collectCoverageFrom: [
'**/*.(tsx|ts)',
'!**/node_modules/**',
'!**/coverage/**',
'!**/*.d.ts'
],
// A preset that is used as a base for Jest's configuration
preset: 'ts-jest',
// An array of directory names to be searched recursively up from the requiring module's location
moduleDirectories: [
"node_modules",
"src"
],
modulePaths: ['<rootDir>'],
moduleNameMapper: {
'\.(css|scss|less)': 'identity-obj-proxy',
'\.(jpg|png|webp|gig|svg|mp4|webm|mp3|m4a|aac)$': 'identity-obj-proxy',
'^~/(.*)': '<rootDir>/src/'
},
testEnvironment: 'jsdom',
} as Config.InitialOptions
然后我们就可以使用下面的命令跑通:
jest --config='./src/__tests__/react/config/test.config.ts' -u --coverage
测试结果
注意事项:
- rootDir指向一定是根路径,即你的项目路径,否则会报错
- 当我们不想测试某一个文件的时候,在testMatch直接把某个测试或者某些测试剔除
testMatch: [
'**/src/__tests__/react/*.test.tsx',
'**/src/__tests__/react/**/*.test.tsx',
'!**/src/__tests__/react/route/route-routes.test.tsx',
// '!**/src/__tests__/react/route/route-matchRoutes.test.tsx',
]
这便是按照配置运行了,再结合我们很早之前讲解到的cli配置(
https://www.toutiao.com/article/7251203760847536698/),那么我们就清楚了单个文件运行测试、多个文件测试以及整体测试,三种方法。以上,便是路由的全部,源码有需要可以从这里获取:https://gitee.com/xifeng-canyang/jest-copy-file-and-video/tree/master/src/__tests__/react/route。