如何使用反应还原钩测试组件?

我有一个简单的 Todo 组件,利用反应还原钩,我正在测试使用酶,但我得到一个错误或一个浅显的渲染如下所述的空对象。

什么是正确的方法来测试组件使用钩从反应-还原?

Todos.js

const Todos = () => {
const { todos } = useSelector(state => state);


return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
};

Js v1

...


it('renders without crashing', () => {
const wrapper = shallow(<Todos />);
expect(wrapper).toMatchSnapshot();
});


it('should render a ul', () => {
const wrapper = shallow(<Todos />);
expect(wrapper.find('ul').length).toBe(1);
});

V1错误:

...
Invariant Violation: could not find react-redux context value;
please ensure the component is wrapped in a <Provider>
...

Js v2

...
// imported Provider from react-redux


it('renders without crashing', () => {
const wrapper = shallow(
<Provider store={store}>
<Todos />
</Provider>,
);
expect(wrapper).toMatchSnapshot();
});


it('should render a ul', () => {
const wrapper = shallow(<Provider store={store}><Todos /></Provider>);
expect(wrapper.find('ul').length).toBe(1);
});

V2测试也会失败,因为 wrapper<Provider>,在 wrapper上调用 dive()将返回与 v1相同的错误。

先谢谢你的帮助!

76333 次浏览

I could test a component which uses redux hooks using enzyme mount facility and providing a mocked store to the Provider:

Component

import React from 'react';
import AppRouter from './Router'
import { useDispatch, useSelector } from 'react-redux'
import StartupActions from './Redux/Startup'
import Startup from './Components/Startup'
import './App.css';


// This is the main component, it includes the router which manages
// routing to different views.
// This is also the right place to declare components which should be
// displayed everywhere, i.e. sockets, services,...
function App () {
const dispatch = useDispatch()
const startupComplete = useSelector(state => state.startup.complete)


if (!startupComplete) {
setTimeout(() => dispatch(StartupActions.startup()), 1000)
}


return (
<div className="app">
{startupComplete ? <AppRouter /> : <Startup />}
</div>
);
}


export default App;

Test

import React from 'react';
import {Provider} from 'react-redux'
import { mount, shallow } from 'enzyme'
import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk';
import App from '../App';


const mockStore = configureMockStore([thunk]);


describe('App', () => {
it('should render a startup component if startup is not complete', () => {
const store = mockStore({
startup: { complete: false }
});
const wrapper = mount(
<Provider store={store}>
<App />
</Provider>
)
expect(wrapper.find('Startup').length).toEqual(1)
})
})

There is another way than @abidibo if you use a function selector defined in another file. You can mock useSelector and your selector function, and then use shallow from enzyme:

Component

import * as React from 'react';
import { useSelector } from 'react-redux';
import Spinner from './Spinner';
import Button from './Button ';
import { getIsSpinnerDisplayed } from './selectors';


const Example = () => {
const isSpinnerDisplayed = useSelector(getIsSpinnerDisplayed);


return isSpinnerDisplayed ? <Spinner /> : <Button />;
};


export default Example;

Selectors

export const getIsSpinnerDisplayed = state => state.isSpinnerDisplayed;

Test

import * as React from 'react';
import { shallow } from 'enzyme';
import Example from './Example';
import Button from './Button ';
import { getIsSpinnerDisplayed } from './selectors';


jest.mock('react-redux', () => ({
useSelector: jest.fn(fn => fn()),
}));
jest.mock('./selectors');


describe('Example', () => {
it('should render Button if getIsSpinnerDisplayed returns false', () => {
getIsSpinnerDisplayed.mockReturnValue(false);


const wrapper = shallow(<Example />);


expect(wrapper.find(Button).exists()).toBe(true);
});
});

It may be a little bit hacky, but it works well for me :)

this is worked for me as well :

import { shallow, mount } from "enzyme";
const store = mockStore({
startup: { complete: false }
});


describe("==== Testing App ======", () => {
const setUpFn = props => {
return mount(
<Provider store={store}>
<App />
</Provider>
);
};


let wrapper;
beforeEach(() => {
wrapper = setUpFn();
});

To mock useSelector use can do this

import * as redux from 'react-redux'


const spy = jest.spyOn(redux, 'useSelector')
spy.mockReturnValue({ username:'test' })

After searching for help I combined some of the methods I found to mock useSelector.

First create a function that does some bootstrapping before your test. Setting up the store with some values that you want to overwrite and mock the useSelector function of react-redux.

I think it is really useful for creating multiple testcases where u see how the store state influences the behaviour of your component.

import configureMockStore from 'redux-mock-store';
import * as Redux from 'react-redux';
import MyComponent from './MyComponent';


const mockSelectors = (storeValues) => {
const mockStore = configureMockStore()({
mobile: {
isUserConnected: false
...storeValues
},
});


jest
.spyOn(Redux, 'useSelector')
.mockImplementation(state => state.dependencies[0](mockStore.getState()));
};


describe('isUserConnected: true', () => {
beforeEach(() => {
mockSelectors({ isUserConnected: true });
component = shallow(<MyComponent />);


test('should render a disconnect button', () => {
expect(component).toBeDefined();
expect(component.find('button')).toBeTruthy();
});
});
});

And the component:

import React from 'react';
import { shallowEqual, useSelector } from 'react-redux';


const MyComponent = () => {
const isConnected = useSelector(selectIsConnected, shallowEqual);


return (
<>
{
showDisconnect ? (
<button type="button" onClick={() => ()}>disconnect</button>
) : null
}
</>
);
};


export default MyComponent;

Below code works for me.

import { configure, shallow} from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import ServiceListingScreen  from './ServiceListingScreen';
import renderer from 'react-test-renderer';
import { createStore } from 'redux';
import serviceReducer from './../store/reducers/services';
import { Provider } from 'react-redux'
 

const store = createStore(serviceReducer  ) ;
configure({adapter: new Adapter()});




const ReduxProvider = ({ children, reduxStore }) => (
<Provider store={reduxStore}>{children}</Provider>
)


describe('Screen/ServiceListingScreen', () => {
it('renders correctly ', () => {
const wrapper = shallow(<Provider store={store}><ServiceListingScreen  /></Provider>);
const tree = renderer.create(wrapper).toJSON();
expect(tree).toMatchSnapshot();
});
   

});


I think this is both the best and the simplest way to mock useSelector hook from Redux store in jest:

  import * as redux from 'react-redux'


const user = {
id: 1,
name: 'User',
}


const state = { user }


jest
.spyOn(redux, 'useSelector')
.mockImplementation((callback) => callback(state))

With the idea being that you are able to provide the state store mock with just a subset of store data.

Testing React Redux Hooks With Enzyme's Shallow Rendering

After reading through all the responses here and digging through the documentation, I wanted to aggregate the ways to test React components using react-redux hooks with Enzyme and shallow rendering.

These tests rely on mocking the useSelector and useDispatch hooks. I'll also provide examples in both Jest and Sinon.

Basic Jest Example

import React from 'react';
import { shallow } from 'enzyme';
import * as redux from 'react-redux';
import TodoList from './TodoList';


describe('TodoList', () => {
let spyOnUseSelector;
let spyOnUseDispatch;
let mockDispatch;


beforeEach(() => {
// Mock useSelector hook
spyOnUseSelector = jest.spyOn(redux, 'useSelector');
spyOnUseSelector.mockReturnValue([{ id: 1, text: 'Old Item' }]);


// Mock useDispatch hook
spyOnUseDispatch = jest.spyOn(redux, 'useDispatch');
// Mock dispatch function returned from useDispatch
mockDispatch = jest.fn();
spyOnUseDispatch.mockReturnValue(mockDispatch);
});


afterEach(() => {
jest.restoreAllMocks();
});


it('should render', () => {
const wrapper = shallow(<TodoList />);


expect(wrapper.exists()).toBe(true);
});


it('should add a new todo item', () => {
const wrapper = shallow(<TodoList />);


// Logic to dispatch 'todoAdded' action


expect(mockDispatch.mock.calls[0][0]).toEqual({
type: 'todoAdded',
payload: 'New Item'
});
});
});

Basic Sinon Example

import React from 'react';
import { shallow } from 'enzyme';
import sinon from 'sinon';
import * as redux from 'react-redux';
import TodoList from './TodoList';


describe('TodoList', () => {
let useSelectorStub;
let useDispatchStub;
let dispatchSpy;


beforeEach(() => {
// Mock useSelector hook
useSelectorStub = sinon.stub(redux, 'useSelector');
useSelectorStub.returns([{ id: 1, text: 'Old Item' }]);


// Mock useDispatch hook
useDispatchStub = sinon.stub(redux, 'useDispatch');
// Mock dispatch function returned from useDispatch
dispatchSpy = sinon.spy();
useDispatchStub.returns(dispatchSpy);
});


afterEach(() => {
sinon.restore();
});
  

// More testing logic...
});

Testing Multiple useSelector Hooks

Testing multiple useSelectors requires us to mock the Redux app state.

var mockState = {
todos: [{ id: 1, text: 'Old Item' }]
};

Then we can mock our own implementation of useSelector.

// Jest
const spyOnUseSelector = jest.spyOn(redux, 'useSelector').mockImplementation(cb => cb(mockState));


// Sinon
const useSelectorStub = sinon.stub(redux, 'useSelector').callsFake(cb => cb(mockState));

You can try redux-saga-test-plan cool redux assertion and running library and lightweight it doest all your heavy lifting of running saga and assertion automatically

const mockState = { rick:"Genius", morty:"dumbAsss"}


expectSaga(yourCoolSaga)
.provide({
select({ selector }, next) {
if (selector) {
return mockState;
}
return next();
}
})
// some assertion here
.put(actions.updateRickMortyPowers(mockState))
.silentRun();

Some of the answers above mocked the useSelector or useDispatch but the redux docs actually advice against that...check out this answer here: https://stackoverflow.com/a/74024854/14432913 which worked for me and followed the example in redux docs