Jest mocking service variable.asObservable return - javascript

I am a junior developer working on a React application using Jest as unit tests framework
I have to test my privateRoute file:
export const PrivateRoute = ({ component: Component, ...rest }) => (
<Route
{...rest}
render={props => {
const currentUser = authenticationService.currentUser;
if (!currentUser) {
// not logged in so redirect to login page with the return url
return (
<Redirect to={{ pathname: "/", state: { from: props.location } }} />
);
}
// authorized so return component
return <Component {...props} />;
}}
/>
);
I am unable to test the condition if (!currentUser) { until the return
Do you happen to have any piece of advice on how to test this line?
I tried to mock authenticationService.currentUser using jest.fn but with no success
Here is the piece of code of authenticationService:
const currentUserSubject = new BehaviorSubject(
JSON.parse(localStorage.getItem("currentUser"))
);
export const authenticationService = {
// ...
currentUser: currentUserSubject.asObservable(),
// ...
};

Unit testing solution for PrivateRoute component using enzyme module.
privateRoute.tsx:
import React from 'react';
import { Route, Redirect } from 'react-router';
import { authenticationService } from './authenticationService';
export const PrivateRoute = ({ component: Component, ...rest }) => (
<Route
{...rest}
render={(props) => {
const currentUser = authenticationService.currentUser;
if (!currentUser) {
return <Redirect to={{ pathname: '/', state: { from: props.location } }} />;
}
return <Component {...props} />;
}}
/>
);
authenticationService.ts:
export const authenticationService = {
currentUser: {},
};
privateRoute.test.ts:
import React from 'react';
import { PrivateRoute } from './privateRoute';
import { mount, shallow } from 'enzyme';
import { MemoryRouter, Redirect, Router } from 'react-router';
import { authenticationService } from './authenticationService';
describe('59825407', () => {
it('should render component if current user exists', () => {
const mProps = { component: jest.fn().mockReturnValueOnce(null) };
const wrapper = mount(
<MemoryRouter>
<PrivateRoute {...mProps}></PrivateRoute>
</MemoryRouter>,
);
expect(wrapper.find(mProps.component).props()).toEqual(
expect.objectContaining({
history: expect.any(Object),
location: expect.any(Object),
match: expect.any(Object),
}),
);
});
it('should redirect if current user does not exist ', () => {
authenticationService.currentUser = undefined as any;
const mProps = { component: jest.fn().mockReturnValueOnce(null), path: '/user' };
const wrapper = mount(
<MemoryRouter initialEntries={['/user']}>
<PrivateRoute {...mProps}></PrivateRoute>
</MemoryRouter>,
);
const history = wrapper.find('Router').prop('history') as any;
expect(history.location.state.from.pathname).toBe('/user');
expect(history.location.pathname).toBe('/');
});
});
Unit test results with 100% coverage:
PASS src/stackoverflow/59825407/privateRoute.test.tsx (16.491s)
59825407
✓ should render component if current user exists (74ms)
✓ should redirect if current user does not exist (12ms)
--------------------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
--------------------------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
authenticationService.ts | 100 | 100 | 100 | 100 | |
privateRoute.tsx | 100 | 100 | 100 | 100 | |
--------------------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 18.683s
Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/59825407

Related

How to test function inside react context

iam having trouble with covering function inside react context
Before that, let me show you the snippet of the code
import {
useCallback,
useEffect,
useState,
createContext,
ReactNode,
FC
} from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import PropTypes from 'prop-types';
import { pushBackRoute } from '../libs/helpers/backRouteHelper';
export interface BottomBarProviderProps {
children: ReactNode;
}
export interface IBottomBarProvider {
selectedMenu: string;
handleClickMenu: (item: { name: string; route: string }) => void;
}
export const defaultValueBottomBarContext = {
selectedMenu: '',
handleClickMenu: () => {}
};
export const BottomBarContext = createContext<IBottomBarProvider>(
defaultValueBottomBarContext
);
export const BottomBarProvider: FC<BottomBarProviderProps> = ({
children,
...props
}) => {
const history = useHistory();
const location = useLocation();
const [selectedMenu, setSelectedMenu] = useState('Belanja');
const handleClickMenu = useCallback(
(item) => {
setSelectedMenu(item.name);
pushBackRoute(location.pathname);
history.push(item.route);
},
[selectedMenu, location?.pathname]
);
useEffect(() => {
Iif (location.pathname === '/marketplace/history') {
setSelectedMenu('Riwayat');
}
}, [location.pathname]);
const value: IBottomBarProvider = {
selectedMenu,
handleClickMenu
};
return (
<BottomBarContext.Provider
value={value}
{...props}
data-testid="bottom-bar-context"
>
{children}
</BottomBarContext.Provider>
);
};
BottomBarProvider.propTypes = {
children: PropTypes.node
};
BottomBarProvider.defaultProps = {
children: null
};
So i managed to cover most of the hooks and render, but the function/handle function inside this context is really hard
And this is my current test code, below :
/* eslint-disable jest/prefer-called-with */
import { render, screen } from '#testing-library/react';
import { BottomBarProvider } from './BottomBarContext';
import '#testing-library/jest-dom/extend-expect';
const mockHistoryPush = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: () => ({
pathname: '/marketplace'
}),
useHistory: () => ({
push: mockHistoryPush
})
}));
describe('BottomBarProvider', () => {
it('should render the children and update the selected menu when handleClickMenu is called', () => {
//Arrange
const children = <div data-testid="children">Hello, World!</div>;
//Act
render(<BottomBarProvider>{children}</BottomBarProvider>);
//Assert
expect(screen.getByTestId('children')).toHaveTextContent('Hello, World!');
});
});
Do you guys have any idea how to cover the function inside this context ?
Mock functions, hooks, and components of the module you don't own are not recommended. The useLocation and useHistory hooks in your case. Incorrect mock will break their functions which causes your tests to pass based on incorrect mock implementation.
Use <MemoryRouter/> component for testing history change. See official example of RTL about how to test react router
You should also create a test component to consume the React Context for testing the <BottomBarProvider/> component and BottomBarContext. The key is to trigger the handleClickMenu event handler by firing a click event on some element of the test component.
E.g.
BottomBarContext.tsx:
import React from 'react';
import {
useCallback,
useEffect,
useState,
createContext,
ReactNode,
FC
} from 'react';
import { useHistory, useLocation } from 'react-router-dom';
// import { pushBackRoute } from '../libs/helpers/backRouteHelper';
export interface BottomBarProviderProps {
children: ReactNode;
}
export interface IBottomBarProvider {
selectedMenu: string;
handleClickMenu: (item: { name: string; route: string }) => void;
}
export const defaultValueBottomBarContext = {
selectedMenu: '',
handleClickMenu: () => { }
};
export const BottomBarContext = createContext<IBottomBarProvider>(
defaultValueBottomBarContext
);
export const BottomBarProvider: FC<BottomBarProviderProps> = ({
children,
...props
}) => {
const history = useHistory();
const location = useLocation();
const [selectedMenu, setSelectedMenu] = useState('Belanja');
const handleClickMenu = useCallback(
(item) => {
setSelectedMenu(item.name);
// pushBackRoute(location.pathname);
history.push(item.route);
},
[selectedMenu, location?.pathname]
);
useEffect(() => {
if (location.pathname === '/marketplace/history') {
setSelectedMenu('Riwayat');
}
}, [location.pathname]);
const value: IBottomBarProvider = {
selectedMenu,
handleClickMenu
};
return (
<BottomBarContext.Provider value={value} {...props} data-testid="bottom-bar-context">
{children}
</BottomBarContext.Provider>
);
};
BottomBarContext.test.tsx:
import { fireEvent, render, screen } from '#testing-library/react';
import '#testing-library/jest-dom';
import React, { useContext } from 'react';
import { MemoryRouter, Route, Switch } from 'react-router-dom';
import { BottomBarContext, BottomBarProvider } from './BottomBarContext';
describe('BottomBarProvider', () => {
test('should pass', async () => {
const TestComp = () => {
const bottomBarContext = useContext(BottomBarContext);
const menuItems = [
{ name: 'a', route: '/a' },
{ name: 'b', route: '/b' },
];
return (
<>
<ul>
{menuItems.map((item) => (
<li key={item.route} onClick={() => bottomBarContext.handleClickMenu(item)}>
{item.name}
</li>
))}
</ul>
<p>selected menu: {bottomBarContext.selectedMenu}</p>
<Switch>
{menuItems.map((item) => (
<Route key={item.route} path={item.route} component={() => <div>{item.name} component</div>} />
))}
</Switch>
</>
);
};
render(
<MemoryRouter initialEntries={['/']}>
<BottomBarProvider>
<TestComp />
</BottomBarProvider>
</MemoryRouter>
);
const firstListItem = screen.getAllByRole('listitem')[0];
fireEvent.click(firstListItem);
expect(screen.getByText('selected menu: a')).toBeInTheDocument();
expect(screen.getByText('a component')).toBeInTheDocument();
});
});
Test result:
PASS stackoverflow/74931928/BottomBarContext.test.tsx (16.574 s)
BottomBarProvider
✓ should pass (116 ms)
----------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------------------|---------|----------|---------|---------|-------------------
All files | 95 | 66.67 | 75 | 94.74 |
BottomBarContext.tsx | 95 | 66.67 | 75 | 94.74 | 50
----------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 18.184 s, estimated 23 s
Coverage HTML reporter:

Unit test for React component containing useRouteMatch

Having the following component:
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;
I want to write unit tests using React Testing Library for it, the problem is that it keeps throwing an error because of useRouteMatch.
Here is my code:
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();
});
});
The error appears at the line with const match = useRouteMatch();, is there a way to include this part in the test?
You should use <MemoryRouter>:
A <Router> that keeps the history of your “URL” in memory (does not read or write to the address bar)
Provide mock locations in the history stack by using the initialEntries props.
Then, use <Route> component to render some UI when its path matches the current URL.
The following example, assuming that the location pathname in the browser current history stack is /one, <Route>'s path prop is also /one, The two matching, rendering MyComponent.
E.g.
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();
});
});
test result:
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
package versions:
"react": "^16.14.0",
"react-router-dom": "^5.2.0"

How can I test React Router with Jest

I'm new to testing with jest and I want to test the following code.
import React from "react";
import "./ButtonLogin.css";
import { Link } from 'react-router-dom';
function ButtonLogin() {
return (
<Link to="/login"> <button className="button-login">Iniciar sesión</button></Link>
)
}
export default ButtonLogin;
import { MemoryRouter } from 'react-router-dom';
import { render, fireEvent, Link } from '#testing-library/react';
import { ButtonLogin } from './ButtonLogin';
it('routes to a new route', async () => {
ButtonLogin = jest.fn();
const { getByText } = render(
<MemoryRouter ButtonLogin={ButtonLogin}>
<Link to="/login">Iniciar sesión</Link>
</MemoryRouter>
);
fireEvent.click(getByText('Iniciar sesión'));
expect(ButtonLogin).toHaveBeenCalledWith('/login');
});
I have performed the following test but it fails and I get the following error in line 9.
routes to a new route
"ButtonLogin" is read-only.
You can use the createMemoryHistory function and Router component to test it. Create a memory history with initial entries to simulate the current location, this way we don't rely on the real browser environment. After firing the click event, assert the pathname is changed correctly or not.
ButtonLogin.tsx:
import React from 'react';
import { Link } from 'react-router-dom';
function ButtonLogin() {
return (
<Link to="/login">
<button className="button-login">Iniciar sesión</button>
</Link>
);
}
export default ButtonLogin;
ButtonLogin.test.tsx:
import { fireEvent, render } from '#testing-library/react';
import React from 'react';
import { Router } from 'react-router-dom';
import ButtonLogin from './ButtonLogin';
import { createMemoryHistory } from 'history';
describe('ButtonLogin', () => {
test('should pass', () => {
const history = createMemoryHistory({ initialEntries: ['/home'] });
const { getByText } = render(
<Router history={history}>
<ButtonLogin />
</Router>
);
expect(history.location.pathname).toBe('/home');
fireEvent.click(getByText('Iniciar sesión'));
expect(history.location.pathname).toBe('/login');
});
});
test result:
PASS examples/69878146/ButtonLogin.test.tsx (10.675 s)
ButtonLogin
✓ should pass (41 ms)
-----------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
ButtonLogin.tsx | 100 | 100 | 100 | 100 |
-----------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 11.722 s, estimated 12 s
package version: "react-router-dom": "^5.2.0"

JSX element type 'ReactElement<any> is not a constructor function for JSX elements. Type 'undefined' is not assignable to type 'Element | null'

I tried the recommended solution of deleting node_modules/ and yarn.lock and reinstalling everything but it did not solve it.
I am making a simple router that renders children based on a prop:
import React, { Fragment } from "react";
type RouterProps = {
currentRoute: string;
children: React.ReactNode;
};
const Router = ({ currentRoute, children }: RouterProps) => {
return React.Children.map(children, child =>
React.cloneElement(child as React.ReactElement<any>, { currentRoute })
);
};
type RouterViewProps = {
route: string;
children: any;
};
Router.View = ({ route, currentRoute, children }: RouterViewProps) => (
<div>{currentRoute === route ? <Fragment>{children}</Fragment> : null}</div>
);
export default Router;
I get the error when trying to use my component in the app:
import React from "react";
import Router from "./components/Router";
import Home from "./components/Home";
function App() {
return (
<div>
<Router currentRoute="home">
<Router.View route="home">
<Home />
</Router.View>
</Router>
</div>
);
}
export default App;
Full error:
TypeScript error in /Users/gonzo/Projects/JS/filex-workshops-registration/src/App.tsx(8,7):
JSX element type 'ReactElement<any, string | ((props: any) => ReactElement<any, string | ... | (new (props: any) => Component<any, any, any>)> | null) | (new (props: any) => Component<any, any
, any>)>[] | null | undefined' is not a constructor function for JSX elements.
Type 'undefined' is not assignable to type 'Element | null'. TS2605
6 | return (
7 | <div>
> 8 | <Router currentRoute="home">
| ^
9 | <Router.View route="home">
10 | <Home />
11 | </Router.View>
The Router component works perfectly in my tests so I don't understand what's different in the app itself.
Router is not a constructor for JSX since it does not return JSX.
const Router = ({ currentRoute, children }: RouterProps) => {
return (
<>
{React.Children.map(children, child =>
React.cloneElement(child as React.ReactElement<any>, { currentRoute })
)}
</>
);
};

How to jest mock a function living within another react component?

I have been trying to write tests for the following react component which returns different components depending on my props:
const Choice: React.FC<States> = props => {
function getChoiceComponent(): JSX.Element {
if (props.choices) {
return <FirstComponent {...props} />;
} else {
return <SecondComponent {...props} />;
}
}
return <>{getChoiceComponent()}</>;
};
How can I mock getChoiceComponent function and test it?
We should test the react component by changing the props and state rather than test the getChoiceComponent method directly. Here is the unit test solution,
index.tsx:
import React from 'react';
import FirstComponent from './first';
import SecondComponent from './second';
type States = any;
const Choice: React.FC<States> = (props) => {
function getChoiceComponent(): JSX.Element {
if (props.choices) {
return <FirstComponent {...props} />;
} else {
return <SecondComponent {...props} />;
}
}
return <>{getChoiceComponent()}</>;
};
export default Choice;
first.tsx:
import React from 'react';
const FirstComponent = () => <div>first component</div>;
export default FirstComponent;
second.tsx:
import React from 'react';
const SecondComponent = () => <div>second component</div>;
export default SecondComponent;
index.test.tsx:
import Choice from './';
import FirstComponent from './first';
import SecondComponent from './second';
import React from 'react';
import { shallow } from 'enzyme';
describe('60152774', () => {
it('should render first component', () => {
const props = { choices: [] };
const wrapper = shallow(<Choice {...props}></Choice>);
expect(wrapper.find(FirstComponent)).toBeTruthy();
});
it('should render second component', () => {
const props = {};
const wrapper = shallow(<Choice {...props}></Choice>);
expect(wrapper.find(SecondComponent)).toBeTruthy();
});
});
Unit test results with coverage report:
PASS stackoverflow/60152774/index.test.tsx
60152774
✓ should render first component (20ms)
✓ should render second component (5ms)
------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------|---------|----------|---------|---------|-------------------
All files | 88.24 | 100 | 50 | 100 |
first.tsx | 75 | 100 | 0 | 100 |
index.tsx | 100 | 100 | 100 | 100 |
second.tsx | 75 | 100 | 0 | 100 |
------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 3.065s
Source code: https://github.com/mrdulin/react-apollo-graphql-starter-kit/tree/master/stackoverflow/60152774

Categories

Resources