I have a simple login component and a MobX store which is supposed to hold some user information. I want to test this integration with Jest. The app is created with Create React App, so I am just building off that for my tests.
My Login component looks like this:
const Login = () => {
const { accountStore, userStore } = useStores();
const { user } = userStore;
return(
<>
{user ?
<>
<h1>{user.fullName}</h1>
<h2>{user.username}</h2>
</>
:
<main className="form-signin">
<Button onClick={accountStore.RedirectToFeide} aria-label='sign-in-with-feide-button'>Sign in with Feide</Button>
<Button onClick={userStore.GetCurrentUser} aria-label='get-info-button'>Get Current User Info</Button>
</main>
}
</>
)
}
export default observer(Login);
My RootStore looks like this and provides the useStores hook:
export class RootStore {
public userStore: UserStore;
public accountStore: AccountStore;
constructor() {
this.userStore = new UserStore(this);
this.accountStore = new AccountStore(this);
}
}
const StoresContext = createContext(new RootStore());
export const useStores = () => useContext(StoresContext);
If the user in the store is present I would like to see their information. If not I would like to see the two buttons.
I have mocked the UserStore in a folder called __mocks__ like this:
class UserStore {
private rootStore: RootStore;
public user?: User;
constructor (rootStore: RootStore) {
makeAutoObservable(this)
this.rootStore = rootStore;
}
#action
GetCurrentUser = async () => {
this.user = {
username: 'username',
fullName: 'Test User',
dateOfBirth: new Date('1994-03-15'),
email: 'test#example.com'
}
}
}
export default UserStore;
When I use jest.mock() in my test the GetCurrentUser method is mocked correctly, and the mocked data is set in the user object. However, I have not been able to reset the user object before doing a new test in any way.
I have tried:
Mocking in beforeEach/afterEach
jest.clearAllMocks(), jest.resetAllMocks() and jest.restoreAllMocks() both within beforeEach and without
Adding and removing describes to isolate tests
The test looks like this:
import React from 'react';
import { fireEvent, render } from '#testing-library/react';
import Login from './Login';
jest.mock('../../stores/UserStore')
beforeEach(() => {
jest.clearAllMocks()
})
describe('User is logged in', () => {
test('Login shows users name after getting current user information', async () => {
const { getByText } = render(<Login />);
fireEvent.click(getByText('Get Current User Info'));
expect(getByText('Test User')).toBeInTheDocument()
})
})
describe('User not logged in', () => {
test('Login should show sign in button', () => {
const { getByText } = render(<Login />)
expect(getByText('Sign in')).toBeInTheDocument();
})
})
Since the first test presses the button and sets the user observable the second test fails. How can I reset the entire mocked store between each test?
I had similar issue because I was using React Context without Provider.
After wrapping the main app with provider like:
const StoreContext = createContext<RootStore | undefined>(undefined);
export function RootStoreProvider({
children,
rootStore,
}: {
children: ReactNode;
rootStore: RootStore;
}) {
return (
<StoreContext.Provider value={root}>{children}</StoreContext.Provider>
);
}
export const useStores = () => useContext(StoresContext);
I also modified render function from #testing-library/react to accept more arguments:
type CustomRenderTypes = RenderOptions & {rootStore?: RootStore;};
function customRender(
ui: ReactElement,
{rootStore, ...options}: CustomRenderTypes = {},
): RenderResult {
const store = rootStore || new RootStore();
return render(ui, {
wrapper: ({ children }) => {
return (
<RootStoreProvider rootStore={store}>
{children}
</RootStoreProvider>
);
},
...options,
});
}
And after that every test case was getting the fresh copy of store and there was no need to reset mocks
Try to include import of the hook you are mocking in your test-file.
OR
You can try to mock it another way: include import of useStores hook in the test. Then you call jest.mock('useStores'). And then you should mock its implementation.
import useStores from '../../stores/UserStore')
jest.mock('useStores')
useStores.mockImplementation(() => someDummyData)
Described solution you can find in docs.
Related
Here is my test
const initialRootState = {
accounts: [mockAccounts],
isLoading: false
}
describe('Account Dashboard', () => {
let rootState = {
...initialRootState
}
const mockStore = configureStore()
const store = mockStore({ ...rootState })
const mockFunction = jest.fn()
jest.spyOn(Redux, 'useDispatch').mockImplementation(() => mockFunction)
jest
.spyOn(Redux, 'useSelector')
.mockImplementation((state) => state(store.getState()))
afterEach(() => {
mockFunction.mockClear()
// Reseting state
rootState = {
...initialRootState
}
})
it('renders correctly', () => {
const wrapper = mount(
<TestWrapper>
<AccountDashboard />
</TestWrapper>
)
console.log(wrapper)
})
})
In my component I am mapping accounts from the state. In my test I am getting the following error TypeError: Cannot read property 'map' of undefined
I would like to test an if statement I am using in my component to ensure it's returning the proper view based on the number of accounts I am receiving.
However, when I console.log(store.getState()) it is printing correctly. What am I doing wrong?
If you're going to test a Redux connected component, I'd recommend steering away from mocking its internals and instead to test it as if it were a React component connected to a real Redux store.
For example, here's a factory function for mounting connected components with enzyme:
utils/withRedux.jsx
import * as React from "react";
import { createStore } from "redux";
import { Provider } from "react-redux";
import { mount } from "enzyme";
import rootReducer from "../path/to/reducers";
/*
You can skip recreating this "store" by importing/exporting
the real "store" from wherever you defined it in your app
*/
export const store = createStore(rootReducer);
/**
* Factory function to create a mounted Redux connected wrapper for a React component
*
* #param {ReactNode} Component - the Component to be mounted via Enzyme
* #function createElement - Creates a wrapper around the passed in component with incoming props so that we can still use "wrapper.setProps" on the root
* #returns {ReactWrapper} - a mounted React component with a Redux store.
*/
export const withRedux = Component =>
mount(
React.createElement(props => (
<Provider store={store}>
{React.cloneElement(Component, props)}
</Provider>
)),
options
);
export default withRedux;
Now, using the above factory function, we can test the connected component by simply using store.dispatch:
tests/ConnectedComponent.jsx
import * as React from "react";
import withRedux, { store } from "../path/to/utils/withRedux";
import ConnectedComponent from "../index";
const fakeAccountData = [{...}, {...}, {...}];
describe("Connected Component", () => {
let wrapper;
beforeEach(() => {
wrapper = withRedux(<ConnectedComponent />);
});
it("initially shows a loading indicator", () => {
expect(wrapper.find(".loading-indicator")).exists().toBeTruthy();
});
it("displays the accounts when data is present", () => {
/*
Ideally, you'll be dispatching an action function for simplicity
For example: store.dispatch(setAccounts(fakeAccountData));
But for ease of readability, I've written it out below.
*/
store.dispatch({ type: "ACCOUNTS/LOADED", accounts: fakeAccountData }));
// update the component to reflect the prop changes
wrapper.update();
expect(wrapper.find(".loading-indicator")).exists().toBeFalsy();
expect(wrapper.find(".accounts").exists()).toBeTruthy();
});
});
This vastly simplifies not having to mock the store/useSelector/useDispatch over and over when you start to test other Redux connected components.
On a side note, you can skip this entirely if you use react-redux's connect function while exporting the unconnected component. Instead of importing the default export, you can import the unconnected component within your test...
Example component:
import * as React from "react";
import { connect } from "react-redux";
export const Example = ({ accounts, isLoading }) => { ... };
const mapStateToProps = state => ({ ... });
const mapDispatchToProps = { ... };
export default connect(mapStateToProps, mapDispatchToProps)(Example);
Example test:
import * as React from "react";
import { mount } from "enzyme";
import { Example } from "../index";
const initProps = {
accounts: [],
isLoading: true
};
const fakeAccountData = [{...}, {...}, {...}];
describe("Unconnected Example Component", () => {
let wrapper;
beforeEach(() => {
wrapper = mount(<Example {...initProps } />);
});
it("initially shows a loading indicator", () => {
expect(wrapper.find(".loading-indicator")).exists().toBeTruthy();
});
it("displays the accounts when data is present", () => {
wrapper.setProps({ accounts: fakeAccountData, isLoading: false });
wrapper.update();
expect(wrapper.find(".loading-indicator")).exists().toBeFalsy();
expect(wrapper.find(".accounts").exists()).toBeTruthy();
});
});
I figured out that my test was working incorrectly due to my selector function in my component being implement incorrectly. So the test was actually working properly!
Note: My team is currently using mocked data(waiting for the API team to finish up endpoints).
Originally the useSelector function in my component(that I was testing) looked like:
const { accounts, isLoading } = useSelector(
(state: RootState) => state.accounts,
shallowEqual
)
When I updated this to:
const { accounts, isAccountsLoading } = useSelector(
(state: RootState) => ({
accounts: state.accounts.accounts,
isAccountsLoading: state.accounts.isLoading
}),
shallowEqual
)
My tests worked - here are my final tests:
describe('App', () => {
let rootState = {
...initialState
}
const mockStore = configureStore()
const store = mockStore({ ...rootState })
jest.spyOn(Redux, 'useDispatch').mockImplementation(() => jest.fn())
jest
.spyOn(Redux, 'useSelector')
.mockImplementation((state) => state(store.getState()))
afterEach(() => {
jest.clearAllMocks()
// Resetting State
rootState = {
...initialState
}
})
it('renders correctly', () => {
const wrapper = mount(
<TestWrapper>
<Dashboard />
</TestWrapper>
)
expect(wrapper.find('[data-test="app"]').exists()).toBe(true)
expect(wrapper.find(verticalCard).exists()).toBe(false)
expect(wrapper.find(horizontalCard).exists()).toBe(true)
})
it('renders multiple properly', () => {
rootState.info = mockData.info
const wrapper = mount(
<TestWrapper>
<Dashboard />
</TestWrapper>
)
expect(wrapper.find(verticalCard).exists()).toBe(true)
expect(wrapper.find(horizontalCard).exists()).toBe(false)
})
})
I learn ReactJs and have a design Composition question about ReactJs higher order component (HOC).
In the code below App.jsx I use this withAuthentication HOC that initializes app core processes. This HOC value is not used in the App.js. Therefore I must suppress all withAuthentication HOC render callbaks and I do that in the shouldComponentUpdate by returning false.
(I use this HOC in many other places to the get HOC's value but not in App.jsx)
File App.jsx:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { compose } from 'recompose';
import { getAlbumData } from './redux/albumData/albumData.actions';
import { getMetaData } from './redux/albumMetaData/albumMetaData.actions';
import Header from './components/structure/Header';
import Content from './components/structure/Content';
import Footer from './components/structure/Footer';
import { withAuthentication } from './session';
import './styles/index.css';
class App extends Component {
componentDidMount() {
const { getMeta, getAlbum } = this.props;
getMeta();
getAlbum();
}
shouldComponentUpdate() {
// suppress render for now boilerplate, since withAuthentication
// wrapper is only used for initialization. App don't need the value
return false;
}
render() {
return (
<div>
<Header />
<Content />
<Footer />
</div>
);
}
}
const mapDispatchToProps = dispatch => ({
getMeta: () => dispatch(getMetaData()),
getAlbum: () => dispatch(getAlbumData()),
});
export default compose(connect(null, mapDispatchToProps), withAuthentication)(App);
The HOC rwapper WithAuthentication below is a standard HOC that render Component(App) when changes are made to Firebase user Document, like user-role changes, user auth-state changes..
File WithAuthentication .jsx
import React from 'react';
import { connect } from 'react-redux';
import { compose } from 'recompose';
import AuthUserContext from './context';
import { withFirebase } from '../firebase';
import * as ROLES from '../constants/roles';
import { setCurrentUser, startUserListener } from '../redux/userData/user.actions';
import { selectUserSlice } from '../redux/userData/user.selectors';
const WithAuthentication = Component => {
class withAuthentication extends React.Component {
constructor() {
super();
this.state = {
authUser: JSON.parse(localStorage.getItem('authUser')),
};
}
componentDidMount() {
const { firebase, setUser, startUserListen } = this.props;
this.authListener = firebase.onAuthUserListener(
authUser => {
this.setState({ authUser });
setUser(authUser);
startUserListen();
},
() => {
localStorage.removeItem('authUser');
this.setState({ authUser: null });
const roles = [];
roles.push(ROLES.ANON);
firebase
.doSignInAnonymously()
.then(authUser => {
if (process.env.NODE_ENV !== 'production')
console.log(`Sucessfully signed in to Firebase Anonymously with UID: ${firebase.getCurrentUserUid()}`);
firebase.doLogEvent('login', { method: 'Anonymous' });
firebase
.userDoc(authUser.user.uid)
.set({
displayName: `User-${authUser.user.uid.substring(0, 6)}`,
roles,
date: firebase.fieldValue.serverTimestamp(),
})
.then(() => {
console.log('New user saved to Firestore!');
})
.catch(error => {
console.log(`Could not save user to Firestore! ${error.code}`);
});
})
.catch(error => {
console.error(`Failed to sign in to Firebase: ${error.code} - ${error.message}`);
});
},
);
}
componentWillUnmount() {
this.authListener();
}
render() {
const { currentUser } = this.props;
let { authUser } = this.state;
// ALl changes to user object will trigger an update
if (currentUser) authUser = currentUser;
return (
<AuthUserContext.Provider value={authUser}>
<Component {...this.props} />
</AuthUserContext.Provider>
);
}
}
withAuthentication.whyDidYouRender = true;
const mapDispatchToProps = dispatch => ({
setUser: authUser => dispatch(setCurrentUser(authUser)),
startUserListen: () => dispatch(startUserListener()),
});
const mapStateToProps = state => {
return {
currentUser: selectUserSlice(state),
};
};
return compose(connect(mapStateToProps, mapDispatchToProps), withFirebase)(withAuthentication);
};
export default WithAuthentication;
My question is will this hit me later with problems or is this ok to do it like this?
I know a HOC is not suppose to be used like this. The WithAuthentication is taking care of Authentication against Firebase and then render on all user object changes both local and from Firestore listener snapshot.
This HOC is used in many other places correctly but App.jsx only need to initialize the HOC and never use it's service.
My question is will this hit me later with problems or is this ok to do it like this?
I have a React UI kit and want to get some functionality to it. I also have some functionality without the UI. Both are working separately, but I cannot manage to work together due to the error
TypeError: Cannot destructure property 'authenticate' of
'Object(...)(...)' as it is undefined.
I have an account object which is the context provider (Accounts.js, shortened for brevity):
import React, { createContext } from 'react'
import { CognitoUser, AuthenticationDetails } from 'amazon-cognito-identity-js'
import Pool from 'UserPool'
const AccountContext = createContext()
const Account = (props) => {
const getSession = async () =>
await new Promise((resolve, reject) => {
...
})
const authenticate = async (Email, Password) =>
await new Promise((resolve, reject) => {
...
})
const logout = () => {
const user = Pool.getCurrentUser()
if (user) {
user.signOut()
}
}
return (
<AccountContext.Provider
value={{
authenticate,
getSession,
logout
}}
>
{props.children}
</AccountContext.Provider>
)
}
export { Account, AccountContext }
And I have SignIn.js Component which throws the error (also shortened):
import React, { useState, useEffect, useContext } from 'react';
import { Link as RouterLink, withRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
import validate from 'validate.js';
import { AccountContext } from 'Accounts.js';
const SignIn = props => {
const { history } = props;
const [status, setStatus] = useState(false);
const { authenticate, getSession } = useContext(AccountContext);
const classes = useStyles();
const [formState, setFormState] = useState({
isValid: false,
values: {},
touched: {},
errors: {}
});
useEffect(() => {
const errors = validate(formState.values, schema);
setFormState(formState => ({
...formState,
isValid: errors ? false : true,
errors: errors || {}
}));
getSession()
.then(session => {
console.log('Session:', session);
setStatus(true);
});
}, [formState.values]);
const handleSignIn = event => {
event.preventDefault();
authenticate(formState.values.email, formState.values.password)
.then(data => {
console.log('Logged in!', data);
//setStatus(true);
})
.catch(err => {
console.error('Failed to login!', err);
//setStatus(false);
})
history.push('/');
};
return (
<div className={classes.root}>
</div>
);
};
SignIn.propTypes = {
history: PropTypes.object
};
export default withRouter(SignIn);
I guess something is wrong with the Accounts.js because the SignIn.js cannot use the authenticate or getSession functions. I need those in the context because other components will render differently when a user is signed in and getSession exactly retrieves this info. Accounts.js is calling against AWS Cognito. I understand how to use variables or states in context but functions seem to work differently. How do I define the functions in Accounts.js to add them to the context so that I can use them in other components as well?
I have tried similar approach in my application.
As per your code, everything is looking fine. The error you have mentioned can be because of wrapping SignIn component wrongly in Provider i.e Account.
Try wrapping SignIn Component inside Account Provider like below:
Import {Account} from './Accounts.js' // Path of Account.js file
<Account> // Account act as Provider as per your code
<SignIn />
...
</Account>
Rest of your code seems fine.
I have a react context like this
UserContext.js
export const UserContext = createContext({
createUser: async () => {
...execute some xhr requests and manipulateData
},
getUser: () => {},
});
const UserProvider ({ children }) => {
const context = {
createUser,
getUser,
};
return (
<UserContext.Provider value={context}>{children}</UserContext.Provider>
);
}
And the following unit test
UserContext.spec.js
import { render } from '#testing-library/react';
import UserProvider, { UserContext } from '#my-contexts/user';
...
let createUser = () => {};
render(
<UserProvider>
<UserContext.Consumer>
{value => {
createUser = value.createUser;
return null;
}}
</UserContext.Consumer>
</UserProvider>
);
await createUser(data);
expect(data).toEqual({ status: 200 });
I am not sure if this way
let createUser = () => {};
render(
<UserProvider>
<UserContext.Consumer>
{value => {
createUser = value.createUser;
return null;
}}
</UserContext.Consumer>
</UserProvider>
);
it is the better technique for "extract" the inner methods exposed in the UserContext (in this case createUser method)
In my App code I use this context like following:
import { useContext } from 'react';
import { UserContext } from '#my-contexts/user';
const someComponent = (props) => {
...
const { createUser } = useContext(UserContext);
const handleCreate = (e) => {
createUser(form);
};
return (
<form>
<label>Username</label>
<input value={form.userName} />
<button type="submit" onClick={handleCreate}> create user </button>
</form>
)
};
But the problem is that I can get the exposed methods from context only if I have a component and get the method using useContext hook.
If I want to test the createUser method in isolation:
Exists a better way for get the methods exposed in context without rendering a provider and consumer and "manually extract" him?
I'm having some troubles testing that a prop is fired on my HOC.
import { connect } from 'react-redux';
import { compose, lifecycle } from 'recompose';
import { fetchCurrentUser } from '../../actions/users';
import { getUser } from '../../reducers/users';
import User from '../../models/User';
export default Component => compose(
connect(state => ({
user: getUser(state),
}),
{ fetchCurrentUser }),
lifecycle({
componentDidMount() {
if (this.props.user instanceof User) return;
this.props.fetchCurrentUser();
},
}),
)(Component);
What I'd like to know is whether or not fetchCurrentUser is trigger when user is NOT a User instance.
So far I have that in my test:
it.only('fetches user if user is not a User instance', () => {
const setup = () => {
const props = {
user: 'string',
fetchCurrentUser: jest.fn(),
};
const enzymeWrapper = mounting(props);
return {
props,
enzymeWrapper,
};
};
// That returns 0 so false
expect(setup().props.fetchCurrentUser.mock.calls.length).toEqual(1);
});
It seems like I can't replace the props doing it this way. If I log this.props in the lifecycle method, I never see user: 'string'
Thanks in advance
You would need to shallow mount the component in order to test its functionality.
it.only('fetches user if user is not a User instance', () => {
const setup = () => {
const props = {
user: 'string',
fetchCurrentUser: jest.fn(),
};
// shallow render the component
const enzymeWrapper = shallow(<Component {...props} />)
return {
props,
enzymeWrapper,
};
};
expect(setup().props.fetchCurrentUser.mock.calls.length).toEqual(1);
});
OK so, with shubham-khatri's help, here's what I did to make it work.
Separated the component into 2 different ones, and tested the one with the call only. That way I could mock the passed props from the test.
Component:
import { connect } from 'react-redux';
import { compose, lifecycle } from 'recompose';
import { fetchCurrentUser } from '../../actions/users';
import { getUser } from '../../reducers/users';
import User from '../../models/User';
const Connected = connect(state => ({
user: getUser(state),
}),
{ fetchCurrentUser });
export const Enhanced = lifecycle({
componentDidMount() {
if (this.props.user instanceof User) return;
this.props.fetchCurrentUser();
},
});
export default Component => compose(
Connected,
Enhanced,
)(Component);
Test:
describe('Fetching user', () => {
const setup = (moreProps) => {
const props = {
fetchCurrentUser: jest.fn(),
...moreProps,
};
const EnhancedStub = compose(
Enhanced,
)(Component);
const enzymeWrapper = shallow(
<EnhancedStub {...props} />,
);
return {
props,
enzymeWrapper,
};
};
it('fetches user if user is not a User instance', () => {
expect(setup().props.fetchCurrentUser.mock.calls.length).toEqual(1);
});
it('does NOT fetch user if user is a User instance', () => {
expect(setup({ user: new User({ first_name: 'Walter' }) }).props.fetchCurrentUser.mock.calls.length).toEqual(0);
});
});
Hope that helps someone.