Spying on React child component method - reactjs

I am trying to test my React component with jest and enzyme. I have a form component that uses react-skylight component. I am triggering .show() function on form submit and just when response from server is successful.
My test is currently like this:
import MyForm from './MyForm';
import Popup from 'react-skylight';
describe('<MyForm />', () => {
it('should show popup on success', () => {
const popupShowSpy = jest.spyOn(Popup.prototype, 'show');
const myForm = mount(<MyForm />);
myForm.update();
myForm.find('form').simulate('submit');
expect(popupShowSpy).toHaveBeenCalled();
});
});
but I am getting an error when I run tests:
expect(jest.fn()).toHaveBeenCalled()
Expected mock function to have been called.
I found here discusion about similar problem, but for me it is not working.
Solution:
Problem was with axios module. It was updating the component, but the response taht was mocked was not resolved, so thanks to this post here, I've managed to write tests. And I wrapped the child components function call in parent component own function and spied on that parent function.
import MyForm from './MyForm';
import Popup from 'react-skylight';
describe('<MyForm />', () => {
it('should show popup on success', async() => {
const popupShowSpy = jest.spyOn(MyForm.prototype, 'showPopup');
const myForm = mount(<MyForm />);
const response = Promise.resolve({
data: {
message: 'Error: some error'
},
status: 400
});
axios.post = jest.fn(() => response);
myForm.find('form').simulate('submit');
await response;
myForm.update(); // <- child component is rendered correctly
expect(popupShowSpy).toHaveBeenCalled();
});
});

Solution:
Problem was with axios module. It was updating the component, but the response that was mocked was not resolved, so thanks to this post here, I've managed to write tests. And I wrapped the child components function call in parent component own function and spied on that parent function.
import MyForm from './MyForm';
import Popup from 'react-skylight';
describe('<MyForm />', () => {
it('should show popup on success', async() => {
const popupShowSpy = jest.spyOn(MyForm.prototype, 'showPopup');
const myForm = mount(<MyForm />);
const response = Promise.resolve({
data: {
message: 'Error: some error'
},
status: 400
});
axios.post = jest.fn(() => response);
myForm.find('form').simulate('submit');
await response;
myForm.update(); // <- child component is rendered correctly
expect(popupShowSpy).toHaveBeenCalled();
});
});

Related

Unhandled error gets swallowed by async event handler in test with react and mocha.js

I have a curious behaviour in my tests when trying to test an async event handler that awaits a promise. I am using Mocha for my test in combination with global-jsdom, #testing-library/react and #testing-library/user-event to render my component and click a button.
If I now have an error inside the awaited function, this error gets swallowed somewhere and Mocha exits with a passed test.
I build a minimal example to show what I mean:
AsyncFail.js:
import React from 'react';
const AsyncFail = () => {
const asyncHandleClick = async () => {
console.log('test')
await callAsync; //not defined, should throw error
}
return <div onClick={asyncHandleClick} data-testid='button'></div>
};
export default AsyncFail;
import React from 'react';
import { render, screen } from '#testing-library/react';
import userEvent from '#testing-library/user-event';
import AsyncFail from 'asyncFailTest';
describe('asyncFail', () => {
let user;
beforeEach( () => {
user = userEvent.setup();
});
afterEach( () => {
})
context('test async failure', () => {
it('should throw ReferenceError', async () => {
render(
<AsyncFail/>
);
await user.click(screen.getByTestId('button'));
});
});
});
The button click only logs 'test' to the console
I was expecting the error in the best case to cause the test to fail. But I would already be happy if the error would be logged to console.error(), because than I could at least as a workaround spy on this call to check for errors.
I am aware that I should wrap the call with a try/catch-block, but I also want to cover the case if that is forgotten by chance.
I know that an error gets thrown because I can attach a listener to process for that:
In test file:
function onUncaught(err){
console.error(err);
}
beforeEach(() => {
process.on('unhandledRejection', onUncaught);
user = userEvent.setup();
});
This logs:
ReferenceError: callAsync is not defined
What is happening here? Am I doing something wrong?

How to test onClick with Jest that is NOT a callback function in props?

I found lots of ways of using mock functions in jest to spy on callback functions that are passed down to a component but nothing on testing a simple onClick that is defined in the same component.
My Example Page:
const ExamplePage: NextPage = () => {
const router = useRouter();
const onClick = (): Promise<void> => {
axios.post(`/api/track`, {
eventName: Event.TRACK_CLICK,
});
router.push("/new-route");
return Promise.resolve();
};
return (
<Container data-testid="container">
<Title>Example Title</Title>
<CreateButton data-testid="create-button" onClick={onClick}>
Create Partner
</CreateButton>
</Container>
);
};
export default ExamplePage;
My current test where I am attempting to get the onClick from getAttribute:
import { fireEvent, render } from "../../../../test/customRenderer";
import ExamplePage from "../../../pages/example-page";
describe("Example page", () => {
it("has a button to create", () => {
const { getByTestId } = render(<ExamplePage />);
const createButton = getByTestId("create-button");
expect(createButton).toBeInTheDocument();
});
it(" the button's OnClick function should be executed when clicked", () => {
const { getByTestId } = render(<ExamplePage />);
// find the button
const createButton = getByTestId("create-button");
// check the button has onClick
expect(createButton).toHaveAttribute("onClick");
// get the onClick function
const onClick = createButton.getAttribute("onClick");
fireEvent.click(createButton);
// check if the button's onClick function has been executed
expect(onClick).toHaveBeenCalled();
});
});
The above fails since there is no onClick attribute only null. My comments in the test highlight my thought process of trying to reach down into this component for the function on the button and checking if it has been called.
Is there any way to test a onClick that is self contained in a react component?
You need to provide mocked router provider and expect that a certain route is pushed to the routers. You also need extract the RestAPI into a separate module and mock it! You can use Dependency Injection, IOC container or import the Api in the component and mock it using jest. I will leave the RestAPi mocking to you.
Mocking router details here: How to mock useRouter
const useRouter = jest.spyOn(require('next/router'), 'useRouter')
describe("", () => {
it("",() => {
const pushMock = jest.fn();
// Mocking Rest api call depends on how you are going to "inject it" in the component
const restApiMock = jest.jn().mockResolvedValue();
useRouter.mockImplementationOnce(() => ({
push: pushMock,
}))
const rendrResult = render(<ExamplePage />);
//get and click the create button
//expect the "side" effects of clicking the button
expect(restApiMock).toHaveBeenCalled();
expect(pushMock).toHaveBeenCalledWith("/new-route");
});
});

Dispatch a Custom Event and test if it was correctly triggered (React TypeScript, Jest)

I am trying to validate a custom event listener that lies inside a react function useEffect hook as shown below:
export interface specialEvent extends Event {
detail?: string
}
function Example() {
React.useEffect(()=>{
document.addEventListener('specialEvent', handleChange)
return () => {
document.removeEventListener('specialEvent',handleChange)
}
})
const handleChange = (event:SpecialEvent) => {
...
}
}
I want to trigger this custom event listener and test it in jest:
it('should trigger "specialEvent" event Listener Properly', async () => {
const specialEvent = new CustomEvent('specialEvent')
const handleChange = jest.fn()
render(<Example />)
await waitFor(() => {
window.document.dispatchEvent(specialEvent)
expect(window.document.dispatchEvent).toHaveBeenNthCalledWith(1, 'specialEvent')
expect(specialEvent).toHaveBeenCalledTimes(1)
})
})
This code gives me the following error:
expect(received).toHaveBeenNthCalledWith(n, ...expected)
Matcher error: received value must be a mock or spy function
Received has type: function
Received has value: [Function dispatchEvent]
As suggested in one of the answers, I tried this:
//Assert Statements
const specialEvent = new CustomEvent('specialEvent');
const handleSelect = jest.fn();
act(() => {
render(<Example />)
});
await waitFor(() => {
window.document.dispatchEvent(specialEvent)
expect(handleSelect).toHaveBeenCalledTimes(1)
});
But this time it says expected call to be 1 but recieved 0.
Can someone help me resolving this?
When testing, code that causes React state updates should be wrapped into act(...). If the handleChange does not cause the React state to update, you don't need to use act.
Besides, it's better not to test the implementation detail, for your case, the test implementation detail statements are:
expect(window.document.dispatchEvent).toHaveBeenNthCalledWith(1, 'specialEvent')
expect(specialEvent).toHaveBeenCalledTimes(1)
Every small change to the implementation detail will cause the test case need to be modified. We should test the UI from the perspective of the user, who doesn't care about the implementation details of the UI, only about rendering the UI correctly.
What you should test is: what happens to the output of the component when the custom event is fired and the state is changed in the event handler.
E.g.
index.tsx:
import React, { useState } from 'react';
export interface SpecialEvent extends Event {
detail?: string;
}
export function Example() {
const [changed, setChanged] = useState(false);
React.useEffect(() => {
document.addEventListener('specialEvent', handleChange);
return () => {
document.removeEventListener('specialEvent', handleChange);
};
});
const handleChange = (event: SpecialEvent) => {
console.log(event);
setChanged((pre) => !pre);
};
return <div>{changed ? 'a' : 'b'}</div>;
}
index.test.tsx:
import { render, screen, act } from '#testing-library/react';
import '#testing-library/jest-dom/extend-expect';
import React from 'react';
import { Example } from './';
describe('70400540', () => {
test('should pass', () => {
const specialEvent = new CustomEvent('specialEvent');
render(<Example />);
expect(screen.getByText('b')).toBeInTheDocument();
act(() => {
window.document.dispatchEvent(specialEvent);
});
expect(screen.getByText('a')).toBeInTheDocument();
});
});
window.document.dispatchEvent(specialEvent) will cause the React state to change, so we wrap it into act(...).
Test result:
PASS examples/70400540/index.test.tsx (11.259 s)
70400540
✓ should pass (59 ms)
console.log
CustomEvent { isTrusted: [Getter] }
at Document.handleChange (examples/70400540/index.tsx:16:13)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 12.655 s
package versions:
"#testing-library/react": "^11.2.2",
"react": "^16.14.0",
"jest": "^26.6.3",
As the error message says, the toHaveBeenNthCalledWith matcher requires a mock or spy to be passed to expect.
However, you probably do not need to make any assertion about window.document.dispatchEvent being called because you know you are calling it on the line above in your test.
For more information, check the docs on toHaveBeenNthCalledWith here: https://jestjs.io/docs/expect#tohavebeennthcalledwithnthcall-arg1-arg2-

How to use jest.spyOn with react testing library

I'm refactoring a class component to a funcional component.
In my test file i was using enzyme, but i'm migrating to react-testing-library
This the test i'm doing using enzyme
it('should change module when clicked button login', () => {
const wrapper = mount(<SignUp setModuleCallback={jest.fn()} />)
const instance = wrapper.instance()
jest.spyOn(instance, 'setModule')
wrapper.find('button#login-button').simulate('click')
expect(instance.setModule).toHaveBeenCalled()
})
And this is what i'm trying to do using react-testing-library
it('should change module when clicked button login', async () => {
const { getByTestId } = render(<SignUp setModuleCallback={jest.fn()} />)
const instance = getByTestId('submit')
jest.spyOn(instance, 'setModule')
const button = await waitFor(() => getByTestId('submit'))
act(() => {
fireEvent.click(button)
})
expect(instance.setModule).toHaveBeenCalled()
})
Here's the error that i'm getting
The philosophy behind RTL is that you should test your components "the way your software is used." With that in mind, is there a way to test your component without explicitly asserting that the callback was invoked?
According to your test name, you expect some "module to change." Is there some way to verify in the DOM that the module was changed? For example, maybe each "module" has a unique title, in which case you could use screen.getByText to assert that the correct module is rendered after clicking the button.
If you want to explicitly assert that a callback function was invoked, are you sure you have to use spyOn for this test? I would try something like this:
it('should change module when clicked button login', async () => {
const mockCallback = jest.fn()
const { getByTestId } = render(<SignUp setModuleCallback={mockCallback} />)
const button = await waitFor(() => getByTestId('submit'))
act(() => {
fireEvent.click(button)
})
expect(mockCallback).toHaveBeenCalled()
})

Mock a dependency of a React component using Jest

I have a react component (CreateForm). The React component depends on a module (Store). The CreateForm has a Cancel button. On clicking the cancel button, the handleCancel function of the Store module should be called.
I wrote a test unsuccessfully using Jest:
test.only('should handle cancel button click', () => {
jest.mock('../../src/store');
const store = require('../../src/store');
const wrapper = shallow(<CreateForm />);
const cancelButton = wrapper.find('button').at(1);
cancelButton.simulate('click');
expect(store.default.handleCancel).toBeCalled();
});
The test failed. The mock function did not get called and the test failed. Does the react component not get this version of the mock? If so, how do I fix the test? Thanks.
My CreateForm component looks something like the below:
import Store from './store';
render() {
return (
<Panel>
<FormControls />
<button onClick={Store.create}>Create</button>
<button onClick={Store.handleCancel}>Cancel</button>
</Panel>
);
}
A second improvised test that works for me is shown below.
test.only('should handle cancel button click', () => {
const store = require('../../src/store').default;
const cancel = store.handleCancel;
store.handleCancel = jest.fn();
const wrapper = shallow(<CreateForm />);
const cancelButton = wrapper.find('button').at(1);
cancelButton.simulate('click');
expect(store.handleCancel).toBeCalled();
store.handleCancel = cancel;
});
The above test works. I am manually mocking the function, doing the test and restoring the function back to its original after the test. Is there a better way or Jest way of writing the above test? Thanks.
This is how I have managed to spy on imported functions using Jest.
Import everything that is imported in the file you're testing.
Mock it in the beforeEach, you can use more complex mocking if you need to return values or whatever.
In the afterEach call jest.clearAllMocks() to reset all the functions to normal to stop any mocking falling through to other tests.
Putting it all together it looks something like this.
import shallow from 'enzyme'
import * as Store from './Store' // This should be the actual path from the test file to the import
describe('mocking', () => {
beforeEach(() => {
jest.spyOn(Store, 'handleCancel')
jest.spyOn(Store, 'create')
})
afterEach(() => {
jest.clearAllMocks();
})
test.only('should handle cancel button click', () => {
const wrapper = shallow(<CreateForm />);
const cancelButton = wrapper.find('button').at(1);
cancelButton.simulate('click');
expect(Store.handleCancel).toBeCalled();
})
})
Also, in case you need to mock a default import you can do so like this. jest.spyOn(Store, 'default')
You forgot to tell jest how to mock the store module, in your case it is just undefined.
const store = require('../../src/store');
jest.mock('../../src/store', () =>({
handleCancel: jest.fn()
}));
test.only('should handle cancel button click', () => {
const wrapper = shallow(<CreateForm />);
const cancelButton = wrapper.find('button').at(1);
cancelButton.simulate('click');
expect(store.default.handleCancel).toBeCalled();//I'm not sure about the default here
});
With this solution you tell jest to mock the store with an object that has the handleCancel function which is a jest spy. On this spy you can then test that it was called.

Resources