【问题标题】:How to test components using new react router hooks?如何使用新的反应路由器钩子测试组件?
【发布时间】:2020-01-26 19:07:27
【问题描述】:

到目前为止,在单元测试中,react 路由器匹配参数被检索为组件的道具。 因此,使用特定的 url 参数来测试考虑某些特定匹配的组件很容易:我们只需要在测试中渲染组件时根据需要精确路由匹配的道具(我为此目的使用酶库)。

我真的很喜欢用于检索路由内容的新钩子,但我没有找到关于如何在单元测试中使用新的反应路由器钩子模拟反应路由器匹配的示例?

【问题讨论】:

    标签: react-router


    【解决方案1】:

    编辑:按照 Catalina Astengo 的 answer 中描述的方式执行此操作的正确方法,因为它使用真正的路由器功能,仅模拟历史/路由状态而不是模拟整个钩子。

    我最终解决它的方法是使用 jest.mock 在我的测试中模拟钩子:

    // TeamPage.test.js
    jest.mock('react-router-dom', () => ({
      ...jest.requireActual('react-router-dom'), // use actual for all non-hook parts
      useParams: () => ({
        companyId: 'company-id1',
        teamId: 'team-id1',
      }),
      useRouteMatch: () => ({ url: '/company/company-id1/team/team-id1' }),
    }));
    

    我使用 jest.requireActual 将 react-router-dom 的真实部分用于除我有兴趣模拟的钩子之外的所有内容。

    【讨论】:

    • 就像一个魅力,这种模式将在我的项目中用于模拟外部模块的精确点而不会破坏一切的许多情况下很有用:)
    • 我从来不知道jest.requireActual 这对我很有帮助!
    • 如果我必须在同一个测试文件中传递不同的 companyId 怎么办
    • 如果您需要为测试套件中的每个测试使用不同的参数,我建议您使用此处提到的 spyOn:stackoverflow.com/a/61665964/2201223
    • 这个答案让我误入歧途,下一个最高投票的答案(这里)[stackoverflow.com/a/58206121/344405] 是让组件进入 URL 包含您的参数的状态的“有福”的方式正在寻找,无需嘲笑。
    【解决方案2】:

    上述解决方案的轻微变化,包括用于更复杂场景的多个参数和查询字符串。这很容易抽象成类似于上面几个的实用函数,可以被其他测试重用。

    短版

          <MemoryRouter
            initialEntries={[
              '/operations/integrations/trello?business=freelance&businessId=1&pageId=1&pageName=Trello',
            ]}
          >
            <Route path="/operations/:operation/:location">
              <OperationPage />
            </Route>
          </MemoryRouter>
    

    加长版:

    下面的示例 sn-ps 包含测试文件、组件和日志的完整示例,以帮助留下一点解释空间。

    包括:

    • 反应 16
    • redux 7
    • react-router-dom 5
    • 打字稿
    • 谢谢
    • 传奇
    • @testing-library/react 11

    operations.spec.tsx

    import React from 'react'
    import { MemoryRouter, Route } from 'react-router-dom'
    import { render, screen } from '@testing-library/react'
    import { Provider } from 'react-redux'
    import { createStore, applyMiddleware, compose } from 'redux'
    import createDebounce from 'redux-debounced'
    import thunk from 'redux-thunk'
    import createSagaMiddleware from 'redux-saga'
    import rootReducer from 'redux/reducers/rootReducer'
    import OperationPage from '../operation'
    import { initialState } from '../mock'
    import '@testing-library/jest-dom' // can be moved to a single setup file
    
    const sagaMiddleware = createSagaMiddleware()
    const middlewares = [thunk, sagaMiddleware, createDebounce()]
    const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
    
    const store = createStore(
      rootReducer,
      // any type only until all reducers are given a type
      initialState as any,
      composeEnhancers(applyMiddleware(...middlewares))
    )
    
    const Wrapper: React.FC = ({ children }) => <Provider store={store}>{children}</Provider>
    
    describe('Operation Page - Route', () => {
      it('should load', async () => {
    
        const Element = () => (
          <MemoryRouter
            initialEntries={[
              '/operations/integrations/trello?business=freelance&businessId=1&pageId=1&pageName=Trello',
            ]}
          >
            <Route path="/operations/:operation/:location">
              <OperationPage />
            </Route>
          </MemoryRouter>
        )
        render(<Element />, { wrapper: Wrapper })
        // logs out the DOM for further testing
        screen.debug()
      })
    })
    
    

    通过operations.tsx 记录和组件。懒得包括这个组件的所有类型(通过打字稿)但超出范围:)

    import React from 'react'
    import { useParams, useLocation } from 'react-router-dom'
    import { connect } from 'react-redux'
    import queryString from 'query-string'
    
    const OperationPage = (): JSX.Element => {
      const { search } = useLocation()
      const queryStringsObject = queryString.parse(search)
      const { operation, location } = useParams<{ operation: string; location: string }>()
    
      console.log(
        '>>>>>queryStringsObject',
        queryStringsObject,
        '\n search:',
        search,
        '\n operation:',
        operation,
        '\n location:',
        location
      )
      return <div>component</div>
    }
    
    const mapStateToProps = (state) => {
      return {
        test: state.test,
      }
    }
    
    export default connect(mapStateToProps, {})(OperationPage)
    
    

    运行测试的终端

    >>>>>queryStringsObject [Object: null prototype] {
      business: 'freelance',
      businessId: '1',
      pageId: '1',
      pageName: 'Trello'
    }
     search: ?business=freelance&businessId=1&pageId=1&pageName=Trello
     operation: integrations
     location: trello
    
    
     PASS  src/__tests__/operations.spec.tsx
      Operation Page - Route
        ✓ should load (48 ms)
    
    Test Suites: 1 passed, 1 total
    Tests:       0 skipped, 1 passed, 1 total
    Snapshots:   0 total
    Time:        2.365 s
    Ran all test suites related to changed files.
    

    【讨论】:

      【解决方案3】:

      我查看了 react-router repo 中的钩子测试,看起来您必须将组件包装在 MemoryRouterRoute 中。我最终做了这样的事情来使我的测试工作:

      import {Route, MemoryRouter} from 'react-router-dom';
      
      ...
      
      const renderWithRouter = ({children}) => (
        render(
          <MemoryRouter initialEntries={['blogs/1']}>
            <Route path='blogs/:blogId'>
              {children}
            </Route>
          </MemoryRouter>
        )
      )
      

      希望有帮助!

      【讨论】:

      • 问题在于模拟新的react-router-dom 钩子。将组件包装在 MemoryRouter 中绝对是您想要对路由器内的任何被测组件执行的操作。创建可重用包装器的模式有很多,例如testing-library.com/docs/example-react-router
      • 这个答案应该被接受,更少干扰,更正确
      • 感谢您的回答和@JensBodal 的评论。当然,文档中有明确的示例,但我似乎总是先跳到 SO,哈哈!
      • Router V6} />
      【解决方案4】:

      我的用例是使用 useLocation() 对自定义挂钩进行单元测试。我必须重写 useLocation 的内部属性,它是只读的。

      
      \\ foo.ts
      
      export const useFoo = () => {
      
         const {pathname} = useLocation();
      
      
      \\ other logic
      
      return ({
                \\ returns whatever thing here
             });
      }
      
      /*----------------------------------*/
      
      \\ foo.test.ts
      
      \\ other imports here
      
      import * as ReactRouter from 'react-router';
      
      
      Object.defineProperty(ReactRouter, 'useLocation', {
         value: jest.fn(),
         configurable: true,
         writable: true,
      });
      
      describe("useFoo", () => {
      
      
             it(' should do stgh that involves calling useLocation', () => {
      
                 const mockLocation = {
                     pathname: '/path',
                     state: {},
                     key: '',
                     search: '',
                     hash: ''
                 };
      
      
               const useLocationSpy =  jest.spyOn(ReactRouter, 'useLocation').mockReturnValue(mockLocation)
      
      
      
                const {result} = renderHook(() => useFoo());
               
                 expect(useLocationSpy).toHaveBeenCalled();
      
      
             });
       });
      
      

      【讨论】:

        【解决方案5】:

        在您的组件中使用如下钩子

        import {useLocation} from 'react-router';
        
        const location = useLocation()
        

        在你对 reactRouter 对象的测试中如下所示

        import routeData from 'react-router';
        
        const mockLocation = {
          pathname: '/welcome',
          hash: '',
          search: '',
          state: ''
        }
        beforeEach(() => {
          jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation)
        });
        

        【讨论】:

        • 不错,以上使用 spyOn 的帮助 谢谢@suchin
        • 谢谢!有用!你是怎么知道routeData的?我在 react-router 文档中找不到它。
        • 感谢小语法更正:beforeEach(() =&gt; { jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation) });
        • @GundamMeister 名称无关紧要,因为它是来自 'react-router's 的默认导出
        • 我用它来模拟 useParams 钩子,其他方法对我不起作用。
        【解决方案6】:

        如果你使用react-testing-library 进行测试,你可以让这个模拟像这样工作。

        jest.mock('react-router-dom', () => ({
            ...jest.requireActual('react-router-dom'),
            useLocation: () => ({ state: { email: 'school@edu.ng' } }),
        }));
        
        export const withReduxNRouter = (
            ui,
            { store = createStore(rootReducer, {}) } = {},
            {
            route = '/',
            history = createMemoryHistory({ initialEntries: [ route ] }),
            } = {}
        ) => {
            return {
            ...render(
                <Provider store={store}>
                <Router history={history}>{ui}</Router>
                </Provider>
            ),
            history,
            store,
            };
        };
        

        你应该在 react-router-dom 被用来渲染你的组件之前模拟它。 我正在探索使其可重复使用的方法

        【讨论】:

        • 我正在测试一个使用 useLocation 挂钩的基本离子应用程序。这非常有效。谢谢。
        • 如果您使用 CRA 创建项目,您可以将 jest.mock 块放入 setupTests.js(ts)
        • 你好@chidimo,你有没有办法让它可重复使用?
        • 我想我做到了。我发了一个帖子,你可以在这里找到smashingmagazine.com/2020/07/react-apps-testing-library
        【解决方案7】:

        如果使用enzyme 库,我找到了一种更简洁的解决问题的方法(使用react-router-dom docs 中的这一部分):

        import React from 'react'
        import { shallow } from 'enzyme'
        import { MemoryRouter } from 'react-router-dom'
        import Navbar from './Navbar'
        
        it('renders Navbar component', () => {
          expect(
            shallow(
              <MemoryRouter>
                <Navbar />
              </MemoryRouter>
            )
          ).toMatchSnapshot()
        })

        【讨论】:

          【解决方案8】:

          我正在尝试获取 useHistory 中的 push 函数是否被调用,但我无法获得模拟函数调用...

          const mockHistoryPush = jest.fn();
          
          jest.mock('react-router-dom', () => ({
              ...jest.requireActual('react-router-dom'),
              useHistory: () => ({
                push: mockHistoryPush,
              }),
            }));
          
          fireEvent.click(getByRole('button'));
          expect(mockHistoryPush).toHaveBeenCalledWith('/help');
          

          它说当按钮有onClick={() =&gt; history.push('/help')}时不会调用mockHistoryPush

          【讨论】:

          • jest mocks 先提升模拟模块,因此您的 mockHistoryPush 在运行时不会被看到。相反,在你的测试中,做类似import * as ReactRouterDom from 'react-router-dom'; jest.spyOn(ReactRouterDom, 'useHistory').returnValue({ push: mockHistoryPush, })
          • @JensBodal 我刚刚尝试过,得到一个“TypeError:无法设置只有 getter 的 [object Object] 的属性 useHistory”,如果我找到解决方案会更新
          • 有关于@JasonRogers 的消息吗? :'(
          • 我目前遇到了同样的问题。似乎不可能模拟/测试这种情况。
          • Mocking history.push 在这里解释:stackoverflow.com/questions/58524183/…
          猜你喜欢
          • 1970-01-01
          • 2021-01-21
          • 2017-05-22
          • 2018-03-20
          • 2019-09-30
          • 1970-01-01
          • 2019-07-08
          • 2021-04-12
          • 2020-02-16
          相关资源
          最近更新 更多