I have the following component in my React-Native application. I have been asked to write a unit test using jest and enzyme for this, but I'm new to unit test. So, how to break this down and write proper tests is beyond my knowledge. Could someone help me with this??
import React, { Component } from 'react';
import { View } from 'react-native';
import { WebView } from 'react-native-webview';
import { Button, Loader, ScreenContainer } from '../../../../../components';
import {
decodeBase64,
hasWordsInString,
setFirstAndFamilyName,
} from '../../../../../library/Utils';
import { url, searchWords, signUpMethods } from '../../../../../config';
import { SIGN_UP_FORM } from '../../../../constants/forms';
// tslint:disable-next-line: max-line-length
const response = 'some-token';
class MyWebView extends Component {
state = {
loaderStatus: true,
};
stopLoader = () => {
this.setState({ loaderStatus: false });
}
startLoader = () => {
this.setState({ loaderStatus: true });
}
displayLoader = () => {
const { loaderStatus } = this.state;
return loaderStatus && <Loader />;
}
render() {
const { navigation, addFormData, setSignUpMethod } = this.props;
return (
<View style={{ flex: 1 }}>
<WebView
source={{ uri: url }}
onLoadStart={() => this.startLoader()}
onLoad={() => this.stopLoader()}
onLoadEnd={(syntheticEvent) => {
const { nativeEvent } = syntheticEvent;
if (nativeEvent.title === 'Consent Platform') {
if (hasWordsInString(nativeEvent.url, searchWords)) {
const { address, ...rest } = decodeBase64(response).data;
const userName = setFirstAndFamilyName(rest.name);
rest.firstName = userName.firstName;
rest.familyName = userName.familyName;
addFormData({ form: SIGN_UP_FORM, data: { values: { ...rest, ...address } } });
setSignUpMethod(signUpMethods.MY_INFO);
navigation.replace('ConfirmName', rest);
}
}
}}
/>
{this.displayLoader()}
</View>
);
}
}
export default MyWebView;
How can I write some proper unit tests for the above code using jest and enzyme? What are the principles that I have to follow? What makes a unit test a better one?
Here is a shallow test, then you can pass in your own dummy data and compare the snapshots, so basically if anyone changes any of your custom components that are used in this component your test will fail
import "react-native"
import React from "react"
import { shallow } from "enzyme"
import MyWebView from "../../App/Components/MyWebView"
test("Should render CustomHeader", () => {
const wrapper = shallow(<MyWebView />)
expect(wrapper).toMatchSnapshot()
})
Related
I have the following react code in my project
import React from 'react';
import { Upload } from 'antd';
const { Dragger } = Upload;
...
<Dragger
accept={ACCEPTED_FORMATS}
beforeUpload={beforeUpload}
data-testid="upload-dragger"
maxCount={1}
onChange={({ file: { status } }) => {
if (status === 'done') onUploadComplete();
}}
progress={progress}
showUploadList={false}
>
{/* here i have a button, code ommited for clarity, if needed i'll post it */}
</Dragger>
And I want to test if the callback function onUploadComplete() was called when file.status is 'done'.
Here is how i am doing the test right now:
I have a jest.mock to simulate a dumb request that will always succeed
import React from 'react';
import { fireEvent, render, waitFor } from '#testing-library/react';
import { act } from 'react-dom/test-utils';
import { DraggerProps } from 'antd/lib/upload';
import userEvent from '#testing-library/user-event';
import UploadCompanyLogo from '../UploadCompanyLogo'; // This is the component where the dragger is placed
jest.mock('antd', () => {
const antd = jest.requireActual('antd');
const { Upload } = antd;
const { Dragger } = Upload;
const MockedDragger = (props: DraggerProps) => {
console.log('log test');
return (
<Dragger
{...props}
action="greetings"
customRequest={({ onSuccess }) => {
setTimeout(() => {
onSuccess('ok');
}, 0);
}}
/>
);
};
return { ...antd, Upload: { ...Upload, Dragger: MockedDragger } };
});
The test itself (same file as the mock), where it renders the component (where antd will be imported), simulate an upload and then checks if the callback has been called.
it('completes the image upload', async () => {
const flushPromises = () => new Promise(setImmediate);
const { getByTestId } = render(elementRenderer({ onUploadComplete }));
const file = new File(['(⌐□_□)'], 'chucknorris.png', { type: 'image/png' });
const uploadDragger = await waitFor(() => getByTestId('upload-dragger'));
await act(async () => {
userEvent.upload(uploadDragger, file);
});
await flushPromises();
expect(onUploadComplete).toHaveBeenCalledTimes(1);
expect(onUploadComplete).toHaveBeenCalledWith();
});
elementRenderer
const elementRenderer = ({
onUploadComplete = () => {}
}) => (
<ApplicationProvider>
<UploadCompanyLogo
onUploadComplete={onUploadComplete}
/>
</ApplicationProvider>
);
ApplicationProvider
import React, { PropsWithChildren } from 'react';
import { ConfigProvider as AntdProvider } from 'antd';
import { RendererProvider as FelaProvider } from 'react-fela';
import { createRenderer } from 'fela';
import { I18nextProvider } from 'react-i18next';
import antdExternalContainer, {
EXTERNAL_CONTAINER_ID,
} from 'src/util/antdExternalContainer';
import { antdLocale } from 'src/util/locales';
import rendererConfig from 'src/fela/felaConfig';
import i18n from 'src/i18n';
const ApplicationProvider = (props: PropsWithChildren<{}>) => {
const { children } = props;
return (
<AntdProvider locale={antdLocale} getPopupContainer={antdExternalContainer}>
<FelaProvider renderer={createRenderer(rendererConfig)}>
<I18nextProvider i18n={i18n}>
<div className="antd-local">
<div id={EXTERNAL_CONTAINER_ID} />
{children}
</div>
</I18nextProvider>
</FelaProvider>
</AntdProvider>
);
};
export default ApplicationProvider;
This is currently not working, but have already been improved with the help of #diedu.
The console.log() I have put in the MockedDragger it's currently not showing. If I put a console.log() in both component and mockedDragger, it prints the component log.
Any tips on how to proceed?
I have already seen this issue and didn’t help.
The first thing you should change is the return you are doing in your mock. You are returning a new component called MockedDragger, not Dragger.
return { ...antd, Upload: { ...Upload, Dragger: MockedDragger } };
The next thing is the event firing. According to this issue you should use RTL user-event library and wrap the call in act
import userEvent from "#testing-library/user-event";
...
await act(async () => {
userEvent.upload(uploadDragger, file);
});
...
and finally, due to some asynchronous code running you need to flush the pending promises before checking the call
const flushPromises = () => new Promise(setImmediate);
...
await flushPromises();
expect(onUploadComplete).toHaveBeenCalled()
this is the full version
import { render, waitFor } from "#testing-library/react";
import App from "./App";
import userEvent from "#testing-library/user-event";
import { act } from "react-dom/test-utils";
import { DraggerProps } from "antd/lib/upload";
jest.mock('antd', () => {
const antd = jest.requireActual('antd');
const { Upload } = antd;
const { Dragger } = Upload;
const MockedDragger = (props: DraggerProps) => {
return <Dragger {...props} customRequest={({ onSuccess }) => {
setTimeout(() => {
onSuccess('ok');
}, 0)
}} />;
};
return { ...antd, Upload: { ...Upload, Dragger: MockedDragger } };
});
it("completes the image upload", async () => {
const onUploadComplete = jest.fn();
const flushPromises = () => new Promise(setImmediate);
const { getByTestId } = render(
<App onUploadComplete={onUploadComplete} />
);
const file = new File(["(⌐□_□)"], "chucknorris.png", { type: "image/png" });
const uploadDragger = await waitFor(() => getByTestId("upload-dragger"));
await act(async () => {
userEvent.upload(uploadDragger, file);
});
await flushPromises();
expect(onUploadComplete).toHaveBeenCalled();
});
Unfortunately, I couldn't set up a codesandbox to show it's working, but let me know if you face any issue and I can push the code to a repo.
I am struggling with this React functional component when writing a unit test, when I run the test nothing is returned from render().
I have tried to remove the functionality from the component to return a plain p tag, however this has made no difference. Completely stuck as to why this won't work.
Here is the component and unit test if anyone is able to assist.
Component:
import React, { useState, Fragment, useEffect, MouseEventHandler } from 'react';
import { AdvancedSearchModal } from './AdvancedSearchModal';
import { useTeamPreferences } from 'common/useTeamPreferences';
import { advancedSearchService } from 'services/advanced-search.service';
import { AdvancedSearchQuery, DocumentType } from 'models/AdvancedSearch';
import './AdvancedSearch.scss';
type CreateFilterProps = {
onAggregateQueryChange: Function
}
export const CreateFilterButton = ({onAggregateQueryChange}: CreateFilterProps) => {
const [isShowDialog, setShowDialog] = useState<boolean>(false);
const [availableDocumentTypes, setAvailableDocumentTypes] = useState<DocumentType[]>([]);
const teamPreferences: any = useTeamPreferences();
useEffect(() => {
if (!teamPreferences.show_advanced_search_button) return;
advancedSearchService.listDocumentTypes()
.then((documentTypes) => setAvailableDocumentTypes(documentTypes));
}, [teamPreferences]);
const handleClick = (e: any): void => {
setShowDialog(!isShowDialog);
e.preventDefault();
};
const closeDialog = (): void => {
setShowDialog(false);
}
const handleAggregateQuery = (aggregateQuery: AdvancedSearchQuery[]): void => {
onAggregateQueryChange(aggregateQuery);
closeDialog();
}
if (!teamPreferences.show_advanced_search_button) {
return null;
}
return (
<Fragment>
<a className="btn btn-xs btn-primary btn_patient-filter--new" onClick={handleClick}>New Filter</a>
{isShowDialog && <AdvancedSearchModal
onClose={closeDialog}
handleAggregateQuery={handleAggregateQuery}
availableDocumentTypes={availableDocumentTypes}
/>}
</Fragment>
);
};
Unit test:
import React from 'react';
import { useTeamPreferences } from 'common/useTeamPreferences';
import { act } from "react-dom/test-utils";
import { render, unmountComponentAtNode } from "react-dom";
import { CreateFilterButton } from 'features/patients/advanced-search/CreateFilterButton';
jest.mock('common/useTeamPreferences.js', () => {
const original = jest. requireActual('common/useTeamPreferences')
return {
__esModule: true,
default: jest.fn(original.default)
}
});
describe('CreateFilterButton', () =>{
let container: any;
let teamPreferencesMock: any;
const handleAdvancedSearchResults = () => {
return [];
}
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
act(() => {
render(<CreateFilterButton
onAggregateQueryChange={handleAdvancedSearchResults}/>, container);
});
});
afterEach(() => {
unmountComponentAtNode(container);
container.remove();
container = null;
});
it('should render', () => {
expect(container).toBeTruthy();
});
});
I am new to Jest and Enzyme & facing an issue. Can someone help?
Here is my jsx file (myTemplate.jsx) :
export default (props) => {
const oneSection = () => {
return <div>Hello</div>
};
return (
props.hasData ? { <div>Hey</div> } : { <div><oneSection/></div> }
)
}
And this is my test file :
import React from "react";
import { shallow } from "enzyme";
import myTemplate from '../myTemplate';
describe("template component", () => {
const props= {
hasData: false,
}
let myComponent = null;
beforeAll(() => {
myComponent = shallow(<myTemplate {...props}/>);
})
test("should render with initial state properly", () => {
expect(myComponent).toMatchSnapshot();
})
})
Now this test case is running successfully. Snapshot is getting created with a div which has oneSection but oneSection is not getting replaced with actual html within it. Basically these lines are not getting covered :
const oneSection = () => {
return <div>Hello</div>
};
How can i cover this piece of code using Jest and enzyme ?
import React from 'react'
import { cleanup, render } from '#testing-library/react'
describe('DMReports', () => {
afterEach(cleanup)
test('DMReports: hasData true', () => {
const { container } = render(
<DMReports hasData={true}/>,
)
expect(container).toMatchSnapshot()
})
test('DMReports: hasData false', () => {
const { container, getByText } = render(
<DMReports hasData={false}/>,
)
expect(container).toMatchSnapshot()
expect(getByText('Hello')).toBeTruthy()
})
})
I am using i18n-js within my expo project to translate my app.
This is how I configure it:
import React from 'react';
import * as Localization from 'expo-localization';
import i18n from 'i18n-js';
export default function configureI18n(translations) {
i18n.fallbacks = true;
i18n.translations = translations;
i18n.locale = Localization.locale;
const [locale, setLocale] = React.useState(Localization.locale);
const localizationContext = React.useMemo(() => ({
t: (scope, options) => i18n.t(scope, { locale, ...options }),
locale,
setLocale,
}), [locale]);
return localizationContext;
}
I pass this to my AppContext and try to use setLocale within my view:
function HomeView(props) {
const { locale, setLocale } = useContext(AppContext);
return (
<View>
<Button
style={{ marginTop: 4 }}
icon="translate"
mode="contained"
title="toggle navigation"
onPress={() => setLocale(locale.includes('en') ? 'fr' : 'en')}
>
{locale.includes('en') ? 'FR' : 'EN'}
</Button>
</View>
);
}
The function is called, but the text is still in english, what am I doing wrong ?
You need to setup the translation in your top level component, like App.js. Then, you have to create 2 json files: fr.json and en.json in /src/locales/.
Finally, in any screen, you have to import i18n and use the t() function to translate strings.
In App.js
import React, { useEffect, useState } from 'react'
import { loadLocale } from './locales/i18n'
export default function App() {
const [theme, setTheme] = useState(null)
useEffect(() => {
init()
}, [])
const init = async () => {
await loadLocale()
}
return (
<AppContainer />
)
}
In i18n.js
import * as Localization from 'expo-localization'
import i18n from 'i18n-js'
i18n.defaultLocale = 'fr'
i18n.locale = 'fr'
i18n.fallbacks = true
export const loadLocale = async () => {
for (const locale of Localization.locales) {
if (i18n.translations[locale.languageCode] !== null) {
i18n.locale = locale.languageCode
switch (locale.languageCode) {
case 'en':
import('./en.json').then(en => {
i18n.translations = { en }
})
break
default:
case 'fr':
import('./fr.json').then(fr => {
i18n.translations = { fr }
})
break
}
break
}
}
}
export default i18n
In HomeView.js
import React from 'react'
import i18n from '../locales/i18n'
function HomeScreen({ navigation }) {
return (
<View style={{ flex: 1 }}>
<Text>{i18n.t('home.welcome')}</Text>
<Text>{i18n.t('home.content')}</Text>
</View>
)
}
export default HomeView
In fr.json
{
"home": {
"welcome": "Bienvenue",
"content": "Du contenu ici"
}
}
In order to change between languages and avoid getting [missing "X.string" translation] error you can add a function like this "changeLanguage" function below:
// Imagine you have spanish and english languages support
import es from './locales/es';
import en from './locales/en';
const availableTranslations = {
es,
en
};
/* This function is useful to load spanish or english language translations and set the corresponding locale */
const changeLanguage = (languageCode) => {
I18n.translations = {
[languageCode]: availableTranslations[languageCode]
};
I18n.locale = languageCode;
};
So I have a mock (typemoq) http call that I'm passing into my react component (mounted with enzyme):
const mockhttp: TypeMoq.IMock<IHttpRequest> = TypeMoq.Mock.ofType<IHttpRequest>();
mockhttp
.setup(x => x.get('/get-all-goal-questions'))
.returns(() => {
return Promise.resolve(mockResponse.object.data);
});
const wrapper = mount(<Goals history={Object} http={mockhttp.object} />);
expect(wrapper.find('#foo')).to.have.lengthOf(1);
However, the mock "Get" isn't being called until after the expected, how can I get the expect to wait until the mock is called to test?
// Edit here is the code under test
let httpCall = this.props.pageHoc.httpRequest -- the call im mocking
import React, { Component } from 'react';
import { Row, Col } from 'react-bootstrap';
import { Animated } from "react-animated-css";
import { Answer, IPageHOC } from '../../interfaces/pageObjects';
// Fonts
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome'
import { faCheck } from '#fortawesome/free-solid-svg-icons'
// Cookies
import cookie from 'react-cookies';
// Google analytics
import ReactGA from 'react-ga';
type GoalsComponent = {
answers: Answer[],
showError:boolean,
showAnimation:boolean,
question:string,
questionId:number
};
type Props = {
history:any,
pageHoc?: IPageHOC
}
export default class Goals extends Component<Props, GoalsComponent>
{
constructor(props: any) {
super(props);
this.state = {
answers : [],
showError: false,
showAnimation:false,
question: "",
questionId: 0
}
}
componentDidMount(){
// Hide nav
this.props.pageHoc.hideRightNav();
this.loadQuestions();
}
loadQuestions(){
// Setup auth
let auth = this.props.pageHoc.externalAuth;
auth.setToken(cookie.load('Email'), cookie.load('Password')).then((x) => {
let httpCall = this.props.pageHoc.httpRequest;
// Headers
httpCall.setHeaders({
Organization: cookie.load('Organization')
});
httpCall.get(`/thrive/goal/get-all-goal-questions`)
.then((x) => {
this.setState({
answers:x.data.goalQuestions[0].answers,
question: x.data.goalQuestions[0].question,
questionId: x.data.goalQuestions[0].id
});
})
.catch((x) => {
console.log(x, "error");
});
});
}
render() {
return (
<ul className="list-group list-group-goals">
{this.state.answers.map((x:Answer) =>
<li className={("list-group-item ") + (x.selected ? "selected" : "")} key={x.id} onClick={() => this.toggleGoal(x.id)}>
{x.answer}
<FontAwesomeIcon icon={faCheck} className={("goal-tick ") + (x.selected ? "goal-tick-red" : "")} />
</li>
)}
</ul>
);
}
}
hmm if you are trying to test async request you should follow what is written here:
https://jestjs.io/docs/en/tutorial-async
for the short version your test should look something like this:
it('works with async/await', async () => {
expect.assertions(1);
const data = await user.getUserName(4);
expect(data).toEqual('Mark');
});
You can do something like this:
fun test = async () => {
const mockhttp: TypeMoq.IMock<IHttpRequest> = TypeMoq.Mock.ofType<IHttpRequest>();
mockhttp
.setup(x => x.get('/get-all-goal-questions'))
.returns(() => {
return Promise.resolve(mockResponse.object.data);
});
const wrapper = await mount(<Goals history={Object} http={mockhttp.object} />);
expect(wrapper.find('#foo')).to.have.lengthOf(1);
}
This will wait for the promise returned by the mocked get function to resolve and the component to render with the latest data.