在任何复杂应用中,测试是一个至关重要的方面。测试不仅仅是为了提高覆盖率,其主要目的是尽可能地模拟实际使用场景。
最近,我需要为一个庞大的ReactJS项目建立测试架构。
让我展示给你我是如何做的。虽然它还不完整,但我想与你分享我的进展。
为什么这么做?
该项目已经在使用Enzyme进行测试。
虽然Enzyme是一个不错的库,但是react-testing-library是测试React组件的更好选择。React团队也推荐使用它。
测试概述 - React
由于许多工程师在同一项目的不同部分上工作,建立一个共同的框架来处理常见用例是至关重要的。
测试场景
测试是任何良好的React应用程序的非常重要的部分。而react-testing-library是测试任何现代React应用程序的推荐方式。
虽然react-testing-library使根据组件的行为轻松直观地进行测试变得很容易,但有时设置要测试的组件可能会变得复杂。
接下来我们看看如何解决不同的场景下的问题
场景1:测试Redux连接的组件
测试仅由props控制的纯组件很容易。但往往情况并非如此。
如果组件依赖于redux状态,那么除非连接到redux状态,否则无法测试所有行为。
那么我们该怎么办呢?
首先,我们需要创建一个可重用的函数来渲染组件。这有点类似于ReactJS中的渲染属性模式。
它将接受一个store和一个初始状态作为参数。这些是你想要使用redux存储来测试组件的值。
代码语言:javascript复制import { render, RenderOptions } from '@testing-library/react';
import React from 'react';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import { initialState as reducerInitialState, reducer, store } from 'reducer';
type RenderConnectedInterface = {
initialState: Partial<typeof reducerInitialState>;
store?: typeof store;
} & RenderOptions;
const renderConnected = (
ui: React.ReactElement,
{
initialState = reducerInitialState,
store = createStore(reducer, initialState),
...renderOptions
}: RenderConnectedInterface = {} as RenderConnectedInterface
) => {
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<Provider store={store}>
{children}
</Provider>
);
return render(ui, { wrapper: Wrapper, ...renderOptions });
};
export default renderConnected;
基本上,我们将store和初始状态作为函数的参数。
然后,我们用Redux提供的Provider将传递的组件包装起来。
现在,不再使用react-testing-library提供的默认渲染方法,你可以使用renderConnected函数测试你的组件,并传递你想要的存储部分。
代码语言:javascript复制import { screen } from '@testing-library/react';
import React from 'react';
import SomeComponent from './SomeComponent.tsx';
import renderConnected from 'utils/renderConnected';
describe('Test SomeComponent', () => {
const initialState = {
name: "your name"
};
it('should show the name properly', () => {
const props = {
// ... pass any additional prop
};
renderConnected(<SomeComponent {...props} />, { initialState });
expect(screen.getByText("your name")).toBeDefined();
});
});
场景2:使用UI库和自定义主题
但问题并没有就此结束。很多时候,我们需要用许多类型的提供者包装我们的根组件。
其中一个例子是material-ui或styled-components的ThemeProvider。
代码语言:javascript复制<ThemeProvider theme={theme}>
<CustomCheckbox defaultChecked />
</ThemeProvider>
现在,如果要测试组件功能,该功能使用提供者传递的值,那么测试将失败!
我们可以使用相同的概念来缓解此问题,并用ThemeProvider包装根组件。
为了缓解这个问题,让我们调整renderConnected函数,将组件包装在ThemeProvider中。
代码语言:javascript复制import { render, RenderOptions } from '@testing-library/react';
import React from 'react';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import { initialState as reducerInitialState, reducer, store } from 'reducer';
// ---- 注意这里
import { ThemeProvider } from 'styled-components';
import { customTheme } from './customTheme'
type RenderConnectedInterface = {
initialState: Partial<typeof reducerInitialState>;
store?: typeof store;
} & RenderOptions;
const renderConnected = (
ui: React.ReactElement,
{
initialState = reducerInitialState,
store = createStore(reducer, initialState),
...renderOptions
}: RenderConnectedInterface = {} as RenderConnectedInterface
) => {
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<ThemeProvider theme={customTheme}> // .................... 注意这里......................... <-
<Provider store={store
}>
{children}
</Provider>
</ThemeProvider>
);
return render(ui, { wrapper: Wrapper, ...renderOptions });
};
export default renderConnected;
现在,我们可以传递任何组件,测试将通过。
场景3:使用React Router进行测试
将任何操作完成后导航到新路由是一种非常常见的做法。
比如说,你希望在登录成功后将用户重定向到首页。
我们该怎么做呢?
我们可以利用react-router提供的MemoryRouter。我们可以传递URL路径并测试我们的组件。
我们稍后将看到它是如何工作的,但首先让我们将其添加到代码中!
修改后的renderConnected版本将如下所示:
代码语言:javascript复制// .. 与之前相同
import { MemoryRouter } from 'react-router-dom';
type RenderConnectedInterface = {
initialState: Partial<typeof reducerInitialState>;
store?: typeof store;
route?: string; // 新加入的!
} & RenderOptions;
const renderConnected = (
ui: React.ReactElement,
{
initialState = reducerInitialState,
store = createStore(reducer, initialState),
route = '/', // 新增加
...renderOptions
}: RenderConnectedInterface = {} as RenderConnectedInterface
) => {
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<ThemeProvider theme={customTheme}>
<Provider store={store}>
<MemoryRouter initialEntries={[route]}>{children}</MemoryRouter> // 新增加!
</Provider>
</ThemeProvider>
);
return render(ui, { wrapper: Wrapper, ...renderOptions });
};
export default renderConnected;
请注意,我们现在将一个新的参数route传递给函数。我们还将我们的children用react-router提供的MemoryRouter包装起来。
测试导航
比如说,你正在测试一个FirstPage,点击按钮后导航到另一页SecondPage。你想测试这种行为。
但问题是SecondComponent尚未挂载....对吗?
一种方法是模拟react-router的useNavigation或history对象。
但有一种更简单的方法。我们将使用react-router-dom的Router来为第二个URL路径挂载一个虚拟组件,并确保它显示在画面中。
代码语言:javascript复制it('Test navigation' , () => {
const ui = renderConnected(
<>
<FirstPage />
<Route path="/second-page">Second Page</Route>
</>,
{ initialState }
);
expect(screen.queryByText('Second Page')).toBeNull();
expect(screen.getByText(/First Page/)).toBeDefined();
// 执行操作
const button = utils.getByText(/Submit/);
fireEvent.click(button);
// 此时应该发生导航,我们应该在第二页上
expect(screen.getByText(/Second Page/)).toBeDefined();
expect(screen.queryByText(/First Page/)).toBeNull();
})
现在,你的测试将通过,并且你将会看到那些甜美的绿色复选框!
通过这些高级测试技巧,你可以更全面地测试你的React应用程序,覆盖各种场景和组件。这有助于确保应用程序的质量和稳定性。
我正在参与2024腾讯技术创作特训营第五期有奖征文,快来和我瓜分大奖!