包含 useRouteMatch 的 React 组件的单元测试

Unit test for React component containing useRouteMatch

具有以下组件:

import React, { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { useRouteMatch, Link } from 'react-router-dom';

interface MyComponentProps {
  myId?: string;
  link?: string;
}

export const MyComponent: React.FunctionComponent<MyComponentProps> = ({
  myId = 'default-id',
  link,
  children
}) => {
  const [myOutlet, setMyOutlet] = useState<HTMLOListElement>();
  const match = useRouteMatch();

  useEffect(() => {
    const outletElement = document.getElementById(myId) as HTMLOListElement;
    if (outletElement) {
      setMyOutlet(outletElement);
    }
  }, [myId]);

  if (!myOutlet) {
    return null;
  }

  return createPortal(
    <li>
      <Link to={link || match.url}>{children}</Link>
    </li>,
    myOutlet
  );
};

export default MyComponent;

我想用 React Testing Library 为它写单元测试,问题是因为 useRouteMatch 一直报错。

这是我的代码:

import { render, screen } from '@testing-library/react';

import { MyComponent } from './my-component';

describe('MyComponent', () => {
  const testId = 'default-id';
  const link = '/route';
  it('should render MyComponent successfully', () => {
    const element = render(<MyComponent myId={testId} link={link} />);
    expect(element).toBeTruthy();
  });
});

错误出现在const match = useRouteMatch();行,有没有办法将这部分包含在测试中?

你应该使用 <MemoryRouter>:

A <Router> that keeps the history of your “URL” in memory (does not read or write to the address bar)

使用 initialEntries 道具在历史堆栈中提供模拟位置。

然后,使用<Route>组件渲染一些UI当它的路径匹配当前URL。

下面的例子,假设浏览器当前历史栈中的location pathname是/one<Route>path prop也是/one,这两个匹配、渲染 MyComponent.

例如

my-component.tsx:

import React, { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { useRouteMatch, Link } from 'react-router-dom';

interface MyComponentProps {
  myId?: string;
  link?: string;
}

export const MyComponent: React.FunctionComponent<MyComponentProps> = ({ myId = 'default-id', link, children }) => {
  const [myOutlet, setMyOutlet] = useState<HTMLOListElement>();
  const match = useRouteMatch();
  console.log('match: ', match);

  useEffect(() => {
    const outletElement = document.getElementById(myId) as HTMLOListElement;
    if (outletElement) {
      setMyOutlet(outletElement);
    }
  }, [myId]);

  if (!myOutlet) {
    return null;
  }

  return createPortal(
    <li>
      <Link to={link || match.url}>{children}</Link>
    </li>,
    myOutlet
  );
};

export default MyComponent;

my-component.test.tsx:

import React from 'react';
import { render, screen } from '@testing-library/react';

import { MyComponent } from './my-component';
import { MemoryRouter, Route } from 'react-router-dom';

describe('MyComponent', () => {
  const testId = 'default-id';
  const link = '/route';
  it('should render MyComponent successfully', () => {
    const element = render(
      <MemoryRouter initialEntries={[{ pathname: '/one' }]}>
        <Route path="/one">
          <MyComponent myId={testId} link={link} />
        </Route>
      </MemoryRouter>
    );
    expect(element).toBeTruthy();
  });
});

测试结果:

 PASS  examples/70077434/my-component.test.tsx (8.433 s)
  MyComponent
    ✓ should render MyComponent successfully (46 ms)

  console.log
    match:  { path: '/one', url: '/one', isExact: true, params: {} }

      at MyComponent (examples/70077434/my-component.tsx:13:11)

------------------|---------|----------|---------|---------|-------------------
File              | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
------------------|---------|----------|---------|---------|-------------------
All files         |    87.5 |    28.57 |     100 |   86.67 |                   
 my-component.tsx |    87.5 |    28.57 |     100 |   86.67 | 18,26             
------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        8.951 s, estimated 9 s

包版本:

"react": "^16.14.0",
"react-router-dom": "^5.2.0"