如何使用新的反应路由器钩子测试组件?
Posted
技术标签:
【中文标题】如何使用新的反应路由器钩子测试组件?【英文标题】:How to test components using new react router hooks? 【发布时间】:2020-01-26 19:07:27 【问题描述】:到目前为止,在单元测试中,react 路由器匹配参数被检索为组件的道具。 因此,使用特定的 url 参数来测试考虑某些特定匹配的组件很容易:我们只需要在测试中渲染组件时根据需要精确路由匹配的道具(我为此目的使用酶库)。
我真的很喜欢用于检索路由内容的新钩子,但我没有找到关于如何在单元测试中使用新的反应路由器钩子模拟反应路由器匹配的示例?
【问题讨论】:
【参考方案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:***.com/a/61665964/2201223
这个答案让我误入歧途,下一个最高投票的答案(这里)[***.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 11operations.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 中的钩子测试,看起来您必须将组件包装在 MemoryRouter
和 Route
中。我最终做了这样的事情来使我的测试工作:
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我的用例是使用 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(() => 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=() => 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 在这里解释:***.com/questions/58524183/…以上是关于如何使用新的反应路由器钩子测试组件?的主要内容,如果未能解决你的问题,请参考以下文章
如何将输入值从子表单组件传递到其父组件的状态以使用反应钩子提交?