昨天提到可以用 Mock Module 的方式来模拟函式或套件的回传值,但有些时候情况没那麽单纯,例如当我们有使用 react-router-dom、redux、styled-components 的 ThemeProvider 等作为外层元件(Wrapper)的情况,当有用了这些 wrapper 後,就可以在自己的元件中取得它们所提供的方法。
具体来说,以 react-router-dom 为例,在 App 的最外层会使用 <BrowserRouter />
来把所有的元件包起来,类似像这样:
// index.tsx
import ReactDOM from 'react-dom';
import App from './App';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Router>
<Switch>
<Route path="/">
<App />
</Route>
</Switch>
</Router>
)
接着我们就可以在 <Router>
元件内的其他组件取得由 react-router-dom 提供的方法,例如 useLocation
:
import { useLocation } from 'react-router-dom';
function App() {
const location = useLocation();
return (
<div className="App">
<header className="App-header">
Your current path is {location.pathname}
</header>
</div>
);
}
export default App;
透过 useLocation
就能够在 App 元件中取得当前路由的 location,但要能够使用 useLocation
有一个前提是 <App />
这个元件需要被包在 <Router />
元件中。
React 中类似用法的例子还很多,例如在 styled-components 中,需要使用 <ThemeProvider />
包起来後才能在内层元件中取得定义好的主题配色;在 recoil 中,需要使用 <RecoilRoot />
包起来後才能在内层元件使用到它提供的 useRecoilState
这个方法。
回到 react-router-dom 的例子,前面有提过要在 <App />
中使用 useLocation
的前提是:<App />
需要是 <Router />
的子元件时才能使用,也就是至少要像这样:
<Router>
<App />
</Router>
现在我们要针对 <App />
元件进行测试时,如果在还没有使用 react-router-dom 之前,原本的写法会像是这样,并且能够正确执行:
import { render, screen } from '@testing-library/react';
import App from './App';
test('render user data successfully', async () => {
render(<App />);
const textElement = screen.getByText(/Your current path is/);
expect(textElement).toBeInTheDocument();
});
但现在因为我们在 App 元件中用了 useLocation
这个方法,所以上面这样的测试会喷错,这个错误通常会像是毁天灭地一般:
但细看这个错误讯息就会发现,它说的是 "TypeError: Cannot read property 'location' of undefined",有经验的开发者很快就会知道,这个错误的意思是我们试着从 undefined 中想要去拿出 location 这个属性,於是就坏了。
在原本 <App />
元件中,可以看到是使用了 const location = useLocation();
来取出 location
,也就是说现在的 useLocation()
是 undefined 的意思,所以才会报出刚刚那个错误。
要解决这个问题很简单,只需要在 render App 元件的地方把 <App />
外也包上 <Router />
就可以了,像是这样:
import { render, screen } from '@testing-library/react';
import App from './App';
import { BrowserRouter as Router } from 'react-router-dom';
test('render user data successfully', async () => {
// 因为 App 中用到了 react-router-dom 的 useLocation
// 所以需要在 render App 的地方先把 <App /> 用 <Router /> 包起来
render(
<Router>
<App />
</Router>
);
const textElement = screen.getByText(/Your current path is/);
expect(textElement).toBeInTheDocument();
});
这时候测试就会顺利的通过了:
一般来说,不论是 react-router-dom 或是先前提到的 redux、styled-components、recoil 等这类套件,都可以透过这样的方式来解决,这也是在做整合测试(integration testing)时很常用到的方式。
但是,读者应该会发现这麽做虽然可以解决问题但非常麻烦,因为这类的 Provider(例如,<Router />
)通常都是包在最外层,也就是很多元件都会直接使用它们提供的方法,如果每次测试元件时,都还需要把欲测试的元件一一包起来,实作有点冗余,要是用的工具比较多时可能还会很大一包,例如:
import { render, screen } from '@testing-library/react';
test('render user data successfully', async () => {
render(
<Router>
<RecoilRoot>
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
</RecoilRoot>
</Router>
);
const textElement = screen.getByText(/Your current path is/);
expect(textElement).toBeInTheDocument();
});
有没有办法不要每次测试元件时都要写这些重复的 wrapper 呢?当然有!
在测试时要转译(render)元件是透过 react-testing-library 提供的 render
方法,因此如果想要省去重复撰写这些 wrapper 的话,就可以从这个 render 下手。
在 react-testing-library 中提供了客制化 render 函式的方法,让开发者可以把想要包起来的 wrapper 都先写在 render 函式中。作法其实不会很难,根据官方的范例可以建立一支 custom-testing-library.tsx
:
// custom-testing-library.tsx
import React, { FC, ReactElement } from 'react';
import { render, RenderOptions } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom';
const AllTheProviders: FC = ({ children }) => {
return <Router>{children}</Router>;
};
const customRender = (
ui: ReactElement,
options?: Omit<RenderOptions, 'wrapper'>
) => render(ui, { wrapper: AllTheProviders, ...options });
export * from '@testing-library/react';
export { customRender as render };
接着在撰写测试的时候,就不要直接 import testing-library 的 render 方法,而是从这个 custom-testing-library.tsx
拿,同时由於 custom-testing-library
有直接 export 了原本的 @testing-library/react
,所以一样可以从 custom-testing-library
中取得 react-testing-library 中原本的内容。
现在就可以把原本的测试改成这样:
import { render, screen } from './custom-testing-library';
import App from './App';
test('render user data successfully', async () => {
render(<App />);
const textElement = screen.getByText(/Your current path is/);
expect(textElement).toBeInTheDocument();
});
这时候,我们就不需要在 render
里面在额外包其他哩哩抠抠的各种 Wrapper 了,因为都已经在客制化的 render 中处理掉了。
<<: 【後转前要多久】# Day28 Angular - 四种资料系结 Binding
sync_diff_inspector是TiDB提供的数据效验的工具。 可以用来比对TiDB与MyS...
前言 Hi, 我是鱼板伯爵今天铁人赛的最後一天了感谢大家的支持,不知道大家对这个架构是不是有一点感觉...
Azure Arc on Server功能列表 升级适用於云端的 Microsoft Defende...
JavaScript(通常缩写为JS)是可以内嵌於网页中,是一个成熟的动态程序语言,在网站里加入互动...
今天来简单说明网路架构的演进 在浏览器出现以前,使用的是C/S(Client-Server mode...