企业级 React 项目的高级测试设置

2024-02-02 23:32:57 浏览数 (1)

在任何复杂应用中,测试是一个至关重要的方面。测试不仅仅是为了提高覆盖率,其主要目的是尽可能地模拟实际使用场景。

最近,我需要为一个庞大的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腾讯技术创作特训营第五期有奖征文,快来和我瓜分大奖!

0 人点赞