Mocking external class method inside React component with jest - reactjs

I'm trying to test component method, which inside performing network call to external resources. After reading docs I still can't figure out how to do so. Can anyone help? Here is my code(some parts hidden for brevity):
My component:
import React from 'react'
import ResourceService from '../../modules/resource-service'
export default class SliderComponent extends React.Component {
setActiveSlide = (activeSlide) => {
ResourceService.getData({
id: activeSlide,
}).then((data) => {
if (data) {
this.setState({
data,
})
}
})
}
}
Resource service:
import axios from 'axios'
export default class ResourceService {
static getData(params) {
return axios.post('/api/get_my_data', params)
.then((resp) => resp.data)
}
}
Desired test (as I understand it):
import React from 'react'
import { mount, configure } from 'enzyme'
import SliderComponent from '../../../app/components/slider'
test('SliderComponent changes active slide when setActiveSlide is
called', () => {
const wrapper = mount(
<SliderComponent />
);
wrapper.instance().setActiveSlide(1);
// some state checks here
});
I need mock ResourceService.getData call inside SliderComponent, and I really can't understand ho to do it...

You can import your ResourceService in your test and mock the method getData with jest.fn(() => ...). Here is an example:
import React from 'react'
import { mount, configure } from 'enzyme'
import ResourceService from '../../../modules/resource-service'
import SliderComponent from '../../../app/components/slider'
test('SliderComponent changes active slide when setActiveSlide is
called', () => {
// you can set up the return value, you can also resolve/reject the promise
// to test different scnarios
ResourceService.getData = jest.fn(() => (
new Promise((resolve, reject) => { resolve({ data: "testData" }); }));
const wrapper = mount(<SliderComponent />);
wrapper.instance().setActiveSlide(1);
// you can for example check if you service has been called
expect(ResourceService.getData).toHaveBeenCalled();
// some state checks here
});

try using axios-mock-adapter to mock the postreq in your test.
It should look something like this (may need a few more tweaks):
import React from 'react'
import { mount, configure } from 'enzyme'
import SliderComponent from '../../../app/components/slider'
import axios from'axios';
import MockAdapter = from'axios-mock-adapter';
test('SliderComponent changes active slide when setActiveSlide is
called', () => {
let mock = new MockAdapter(axios)
//you can define the response you like
//but your params need to be accordingly to when the post req gets called
mock.onPost('/api/get_my_data', params).reply(200, response)
const wrapper = mount(
<SliderComponent />
);
wrapper.instance().setActiveSlide(1);
// some state checks here
});
make sure to check the docs of axios-mock-adapter

Related

Jest Mock returns undefined instead of value

I am using Jest to test a react component. I am trying to mock a function from other dependency. The function from dependency should return an array, but it is showing undefined on the console.
Below file is the tsx file, when I click the button, it should call the dependency function to get the list of the Frames.
ExitAppButton.tsx:
import React, { useContext, useState } from 'react';
import { TestContext } from '../ContextProvider';
import { useDispatch } from 'react-redux';
const ExitAppButton = (props: any): JSX.Element => {
const { sdkInstance } = useContext(TestContext);
const exitAppClicked = () => {
const appList = sdkInstance.getFrames().filter((app: any) => {app.appType === "Test App"}).length}
test file, SignOutOverlay.test.tsx:
import * as React from 'react';
import { fireEvent, render, screen } from '#testing-library/react';
import SignOutOverlay from '.';
import ExitAppButton from './ExitAppButton';
import { TestContext } from '../ContextProvider';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
const api = require('#praestosf/container-sdk/src/api');
const mockStore = configureStore([]);
jest.mock('#praestosf/container-sdk/src/api');
api.getFrames.mockReturnValue([{appType:"Test App"},{appType:"Test App"},{appType:"Not Test App"}]);
describe('Test Exit app Button', () => {
const renderExitAppButton = () => {
const store = mockStore([{}]);
render(
<Provider store={store}>
<TestContext.Provider value={{ sdkInstance: api }}>
<SignOutOverlay>
<ExitAppButton/>
</SignOutOverlay>
</TestContext.Provider>
</Provider>
);
};
it('should to be clicked and logged out', () => {
renderExitAppButton();
fireEvent.click(screen.getByTestId('exit-app-button-id'));
});
This is the dependency file, api.js
const getFrames = () => {
let frames = window.sessionStorage.getItem('TestList');
frames = frames ? JSON.parse(frames) : [];
return frames
};
const API = function () { };
API.prototype = {
constructor: API,
getFrames
};
module.exports = new API();
I mocked the getFrame function to return an array of 3 objects, but when running the test case, it is returning undefined. Below error was showing:
TypeError: Cannot read property 'filter' of undefined
Am I mocking this correct?
I think it's because api.getFrames is undefined and not a mock.
Try changing your mock statement to this:
jest.mock('#praestosf/container-sdk/src/api', () => ({
getFrames: jest.fn(),
// add more functions if needed
}));
Turns out, I have the other file with the same test name which is causing the problem. I am beginner for Jest, a tip for developer like me, we should always run test case file alone using
jest file.test.tsx
Not all files at a time:
jest

trying to set value to context provider for a cypress test spec

the challenge am having is explained below
Am trying to set a draft_id value after the submit-recipients data-testid is clicked.
The beforementioned value is passed to a React ContextProvider that wraps my component named SendSurvey which in fact uses the draft_id value
My 2 questions are
How do I import the SendSurvey component to the test spec written below?
I have tried out doing this import SendSurvey from '.../../src/website/Surveys/SendSurvey' in my cypress test files but I get this import error
Just as a side note, I had imported this import { mount } from '#cypress/react' but it caused my cy.visitto fail
How do I wrap my ContextProvider around the SendSurvey component (assuming we imported this component successfully) and pass in the draft_id value to its ContextProvider?
Worth mentioning that I have imported React and createContext hooks from React successfully as such
import * as React from 'react'
import { createContext } from 'react'
/*lines skipped here*/
//what i want to do is to create a context provider and
// pass in the value of the draft id to be used to get draft data
const SendSurveyContext = createContext({})
const SendSurveyProvider = SendSurveyContext.Provider
The actual test spec
describe('Send Message To all contacts', () => {
beforeEach(() => {
cy.intercept('GET', `**/surveys/1`, {
fixture: 'surveys/survey_draft.json',
})
cy.intercept('GET', `**/surveys/1/survey_questions?**`, {
fixture: 'surveys/survey_questions_draft.json',
})
})
it.only('should successfully select all contacts from the api', () => {
cy.intercept('POST', '**/drafts', {
fixture: 'sendsurveys/contacts_draft_success.json',
}).as('createContactsDraft')
cy.intercept('GET', '**/contacts?**', {
fixture: 'sendsurveys/all_contacts.json',
}).as('fetchAllContacts')
cy.visit('/send-survey/1')
cy.get('[data-testid=all-contacts]').click()
cy.wait('#fetchAllContacts')
cy.get('[data-testid=submit-recipients]').click()
cy.contains('Successfully added the contacts.')
// this is where I would like to wrap my context provider around the `SendSurvey` component and
// pass in the draft_id value
})
})
import { mount } from '#cypress/react'
import SendSurvey from '.../../src/website/Surveys/SendSurvey'
import sendSurveyprops from '../fixtures/surveys/sendSurvey/sendSurveyProps'
import draftId from '../fixtures/surveys/sendSurvey/draftId'
describe('Send Message To all contacts', () => {
beforeEach(() => {
cy.intercept('GET', `**/surveys/1`, {
fixture: 'surveys/survey_draft.json',
})
cy.intercept('GET', `**/surveys/1/survey_questions?**`, {
fixture: 'surveys/survey_questions_draft.json',
})
})
mount(
<UserContext.Provider value={draftId}>
<SendSurvey props={sendSurveyprops} />
</UserContext.Provider>
)
it.only('should successfully select all contacts from the api', () => {
cy.intercept('POST', '**/drafts', {
fixture: 'sendsurveys/contacts_draft_success.json',
}).as('createContactsDraft')
cy.intercept('GET', '**/contacts?**', {
fixture: 'sendsurveys/all_contacts.json',
}).as('fetchAllContacts')
cy.get('[data-testid=all-contacts]').click()
cy.wait('#fetchAllContacts')
cy.get('[data-testid=submit-recipients]').click()
cy.contains('Successfully added the contacts.')
})
})
This is a working implementation.

FE: Unit testing window.location.href changing on button click (test fails)

I am trying to test when the window.location.href changes after a button is clicked using react-testing-library. I have seen examples online where you manually update window.location.href inside of a test case as so window.location.href = 'www.randomurl.com' and then follow it with expect(window.location.href).toEqual(www.randomurl.com). While this indeed will pass, I want to avoid this as I'd rather simulate the user actions instead of injecting the new value into the test. If I do that, even if I remove my button click (which is what will actually trigger the function call) the expect will still pass because I have anyway manually updated the window.location.href in my test
What I've opted for is having goToThisPage func (which will redirect the user) to be placed outside of my functional component. I then mock goToThisPage in my test file and in my test case check whether it has been called. I do know that the goToThisPage is being triggered because I included a console.log and when I run my tests I see it in my terminal. Nonetheless, the test still fails. I have been playing around with both spyOn and jest.doMock/mock with no luck
component.js
import React from 'react'
import { ChildComponent } from './childcomponent';
export const goToThisPage = () => {
const url = '/url'
window.location.href = url;
console.log('reached');
};
export const Component = () => {
return (<ChildComponent goToThisPage={ goToThisPage }/>)
}
export default Component;
Test file:
import * as Component from './component'
import userEvent from '#testing-library/user-event';
jest.doMock('./component', () => ({
goToThisPage: jest.fn(),
}));
describe('goToThisPage', () => {
test('should call goToThisPage when button is clicked', async () => {
const goToThisPageSpy = jest.spyOn(Component, 'goToThisPage');
const { container, getByTestId } = render(<Component.Component />);
userEvent.click(screen.getByTestId('goToThisPage')); // this is successfully triggered (test id exists in child component)
expect(goToThisPageSpy).toHaveBeenCalled();
// expect(Component.goToThisPage()).toHaveBeenCalled(); this will fail and say that the value must be a spy or mock so I opted for using spy above
});
});
Note: when I try to just do jest.mock I got this error Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.
When testing out with jest.doMock the error disappeared but the actual test fails.
I am open to hear more refined ideas of solving my issue if someone believes this solution could be improved. Thanks in advance
Edit:
This is another approach I have tried out
import { Component, goToThisPage } from './component'
import userEvent from '#testing-library/user-event';
describe('goToThisPage', () => {
test('should call goToThisPage when button is clicked', async () => {
const goToThisPageSpy = jest.spyOn(Component, 'goToThisPage');
// I am not certain what I'd put as the first value in the spy. Because `goToThisPage` is an external func of <Component/> & not part of the component
const { container, getByTestId } = render(<Component />);
userEvent.click(screen.getByTestId('goToThisPage'));
expect(goToThisPageSpy).toHaveBeenCalled();
});
});
Save yourself the headache and split the goToThisPage function into its own file. You seem to be mocking the goToThisPage function fine but when the Component is rendered with react testing library it doesn't seem render with the mocked function but defaults to what the function would normally do. This easiest way would be just to mock the function from its own file. If you truly want to keep the function in the same file you will need to make some adjustments, see (example #2) but I do not recommend this path.
See below for examples
Example 1: (Recommended) Split function into it's own file
Component.spec.jsx
import React from "react";
import Component from "./Component";
import { render, screen } from "#testing-library/react";
import userEvent from "#testing-library/user-event";
import * as goToThisPage from "./goToThisPage";
jest.mock('./goToThisPage');
describe("goToThisPage", () => {
test("should call goToThisPage when button is clicked", async () => {
const goToThisPageSpy = jest.spyOn(goToThisPage, 'default').mockImplementation(() => console.log('hi'));
render(<Component />);
userEvent.click(screen.getByTestId("goToThisPage"));
expect(goToThisPageSpy).toHaveBeenCalled();
});
});
goToThisPage.js
export const goToThisPage = () => {
const url = "/url";
window.location.href = url;
};
export default goToThisPage;
Component.jsx
import React from "react";
import ChildComponent from "./ChildComponent";
import goToThisPage from "./goToThisPage";
export const Component = () => {
return <ChildComponent goToThisPage={goToThisPage} />
};
export default Component;
Example 2: (Not Recommend for React components!)
We can also get it working by calling the goToThisPage function via exports. This ensures the component is rendered with our spyOn and mockImplementation. To get this working for both browser and jest you need to ensure we run the original function if it's on browser. We can do this by creating a proxy function that determines which function to return based on a ENV that jest defines when it runs.
Component.jsx
import React from "react";
import ChildComponent from "./ChildComponent";
export const goToThisPage = () => {
const url = "/url";
window.location.href = url;
};
// jest worker id, if defined means that jest is running
const isRunningJest = !!process.env.JEST_WORKER_ID;
// proxies the function, if jest is running we return the function
// via exports, else return original function. This is because
// you cannot invoke exports functions in browser!
const proxyFunctionCaller = (fn) => isRunningJest ? exports[fn.name] : fn;
export const Component = () => {
return <ChildComponent goToThisPage={proxyFunctionCaller(goToThisPage)} />
};
export default Component;
Component.spec.jsx
import React from "react";
import { render, screen } from "#testing-library/react";
import userEvent from "#testing-library/user-event";
describe("goToThisPage", () => {
test("should call goToThisPage when button is clicked", async () => {
const Component = require('./Component');
const goToThisPageSpy = jest.spyOn(Component, 'goToThisPage').mockImplementation(() => console.log('hi'));
render(<Component.default />);
userEvent.click(screen.getByTestId("goToThisPage"));
expect(goToThisPageSpy).toHaveBeenCalled();
});
});
You can move the function proxy to it's own file but you need to pass exports into the proxy function as exports is scoped to it's own file.
Example code
// component.js
import React from "react";
import ChildComponent from "./ChildComponent";
import proxyFunctionCaller from "./utils/proxy-function-caller";
export const goToThisPage = () => {
const url = "/url";
window.location.href = url;
};
export const Component = () => {
return <ChildComponent goToThisPage={proxyFunctionCaller(typeof exports !== 'undefined' ? exports : undefined, goToThisPage)} />
};
export default Component;
// utils/proxy-function-caller.js
// jest worker id, if defined means that jest is running
const isRunningJest = !!process.env.JEST_WORKER_ID;
// proxies the function, if jest is running we return the function
// via exports, else return original function. This is because
// you cannot invoke exports functions in browser!
const proxyFunctionCaller = (exports, fn) => isRunningJest ? exports[fn.name] : fn;
export default proxyFunctionCaller;
There are other ways to do this but I would follow the first solution as you should be splitting utility functions into it's own files anyway. Goodluck.
Example 3 for #VinceN
You can mock a function that lives in the same file using the below example files.
SomeComponent.tsx
import * as React from 'react';
const someFunction = () => 'hello world';
const SomeComponent = () => {
return (
<div data-testid="innards">
{someFunction()}
</div>
)
}
export default SomeComponent;
SomeComponent.spec.tsx
import SomeComponent from './SomeComponent';
import { render, screen } from "#testing-library/react";
jest.mock('./SomeComponent', () => ({
__esModule: true,
...jest.requireActual('./SomeComponent'),
someFunction: jest.fn().mockReturnValue('mocked!')
}));
describe('<SomeComponent />', () => {
it('renders', () => {
render(<SomeComponent />);
const el = screen.getByTestId('innards');
expect(el.textContent).toEqual('mocked!');
});
});
You exporting both functions and then defining a default export of the Component itself is what's causing the problem (which is mixing up default and named exports).
Remove export default Component; and change the top import in your test file to import {Component, goToThisPage} from './component'. That said I'm not sure you even need to export goToThisPage (for the Jest test at least).

Context API Unit test failing with TypeError

I have a HOC component WithContext (in a file conveniently named withContext.js) as follows
import React from 'react';
import { DEFAULT_STATE } from './store';
const MyContext = React.createContext(DEFAULT_STATE);
export function WithContext(Component) {
return function WrapperComponent(props) {
return (
<MyContext.Consumer>
{state => <Component {...props} context={state} />}
</MyContext.Consumer>
);
};
};
and a component that uses it as follows
import React from "react";
import { WithContext } from './withContext';
const MyComp = (context) => {
return (
<div className="flex dir-col" id="MyComp">
<p>This is a test</p>
</div>
)
};
export default WithContext(MyComp);
I also have a unit test that aims to test this MyComp component. The unit test follows
import React from "react";
import {shallow} from "enzyme";
import Enzyme from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import { WithContext } from './withContext';
// We need to configure our DOM
import jsdom from 'jsdom'
const {JSDOM} = jsdom;
const {document} = (new JSDOM('<!doctype html><html><body></body></html>')).window;
global.document = document;
global.window = document.defaultView
Enzyme.configure({
adapter : new Adapter()
})
beforeEach(() => {
jest.resetModules()
})
//Takes the context data we want to test, or uses defaults
const getMyContext = (context = {
state : {}
}) => {
// Will then mock the MyContext module being used in our MyComp component
jest.doMock('withContext', () => {
return {
MyContext: {
Consumer: (props) => props.children(context)
}
}
});
// We will need to re-require after calling jest.doMock.
// return the updated MyComp module that now includes the mocked context
return require('./MyComp').default;
};
describe("MyComp component loading check", () => {
test("Renders the MyComp component correctly", () => {
const MyCompContext = getMyContext();
const wrapper = shallow(<MyComp/>);
// make sure that we are able to find the header component
expect(wrapper.find(".flex").hostNodes().length).toBe(1);
});
});
However this test keeps failing with the message
TypeError: (0 , _withContext.WithContext) is not a function
};
export default WithContext(MyComp);
Can you please let me know what is wrong here ?
Thanks
Looks like you are mocking withContext with jest.doMock but the mock you are returning from the factory function does not contain a WithContext function.
Then when you require('./MyComp').default it is using the withContext mock within your MyComp module and failing when it tries to export default WithContext(MyComp); since the withContext mock does not define a WithContext function.

How to test React component with AJAX in componentWillMount

I have a React component where some data is fetched via HTTP during componentWillMount.
This is done pretty much like explained here using the Redux Thunk middleware.
The request is done with axios.
So initially a loading state is set and the data will be set as soon as the request returns.
class SomeComponent extends Component {
//...
componentWillMount() {
store.dispatch(fetchInitialData())
}
// ...
}
The action doing the fetch:
export function fetchInitialData() {
return dispatch => {
return axios.get('https://our.api')
.then(res => {
dispatch(handleSuccess(res.data));
})
.catch(error => dispatch(handleError(error)));
}
}
The test is written with Jest and Enzyme and axios is mocked with axios-mock-adapter.
import Enzyme, { mount } from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'
import configureMockStore from 'redux-mock-store'
import axios from 'axios'
import MockAdapter from 'axios-mock-adapter'
import thunk from 'redux-thunk'
import response from 'testdata/api/response.json'
Enzyme.configure({
adapter: new Adapter()
});
function createMockStore() {
const mockStore = configureMockStore([thunk]);
return mockStore({});
}
describe('some component', () => {
it('should render ...', () => {
const mock = new MockAdapter(axios);
mock.onGet('/our.api').reply(200, response);
const store = createMockStore();
const enzymeWrapper = mount(<SomeComponent store={store}/>);
console.log('enzymeWrapper', enzymeWrapper.html());
});
});
The problem is that the html of enzymeWrapper always contains only the representation of the initial state. The initial HTML never changes according to the new state from the response.
When I add logging to the reducers and services I see that the mocked requests executes and the state is filled with the mocked response.
But the component does not re-render with the new state data.
I want to point out that this is only an issue in the test. In the actual app everything works fine.
To me it seems that I need to somehow wait for the state changes to get applied. But I don't know how.

Resources