Jest + React: IntersectionObserver mock not working? - javascript

In my component.test.js, I tried mocking the IntersectionObserver by doing something like this:
const mock = ()=>({
observe: jest.fn(),
disconnect: jest.fn()
});
window.IntersectionObserver = jest.fn().mockImplementation(mock);
describe("Component", ()=>{
it("test 1", ()=>{
render(<Component/>);
...
}
});
My component.js looks something like this (it does that infinite pagination thing):
//ref to last item loaded >> load more items once it enters view
const observer = useRef();
const lastEntryRef = useCallback((node)=>{
...
observer.current.disconnect(); //ERROR LINE
}
...
);
When I run the tests, however, I get TypeError: observer.current.disconnect is not a function; same goes for observer.current.observe() if it runs. I tried testing it inside the it() block of component.test.js itself by instantiating an IntersectionObserver and then calling those methods and the same message showed when I re-ran the tests, so the errors look unrelated to how IntersectionObserver was set up in component.js. Am I not mocking IntersectionObserver correctly? If so, how do I fix it?

I recommend you to replace the arrow function for a normal function because you need to use the new operator to create an InterceptionObserver object:
const mock = function() {
return {
observe: jest.fn(),
disconnect: jest.fn(),
};
};
//--> assign mock directly without jest.fn
window.IntersectionObserver = mock;
The you can check if window.InterceptionObserver.observe has been called.

Related

Matcher error: received value must be a mock or spy function

I'm writing tests (with Jest and React Testing Library) for a form React component. I have a method that runs on form submit:
const onSubmit = (data) => {
// ...
setIsPopupActive(true);
// ...
};
and useEffect that runs after isPopupActive change, so also on submit:
useEffect(() => {
if (isPopupActive) {
setTimeout(() => {
setIsPopupActive(false);
}, 3000);
}
}, [isPopupActive]);
In the test, I want to check, whether the popup disappears after 3 seconds. So here's my test:
it('Closes popup after 3 seconds', async () => {
const nameInput = screen.getByPlaceholderText('Imię');
const emailInput = screen.getByPlaceholderText('Email');
const messageInput = screen.getByPlaceholderText('Wiadomość');
const submitButton = screen.getByText('Wyślij');
jest.useFakeTimers();
fireEvent.change(nameInput, { target: { value: 'Test name' } });
fireEvent.change(emailInput, { target: { value: 'test#test.com' } });
fireEvent.change(messageInput, { target: { value: 'Test message' } });
fireEvent.click(submitButton);
const popup = await waitFor(() =>
screen.getByText(/Wiadomość została wysłana/)
);
await waitFor(() => {
expect(popup).not.toBeInTheDocument(); // this passes
expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 3000);
});
});
However, I'm getting the error:
expect(received).toHaveBeenCalledTimes(expected)
Matcher error: received value must be a mock or spy function
Received has type: function
Received has value: [Function setTimeout]
What am I doing wrong?
Jest 27 has breaking changes for fakeTimers. It seems Jest contributors doesn't update documentation on time. This comment on Github issues confirms it. Moreover, here related PR.
Well, you can solve your problem by two ways.
Configure Jest to use legacy fake timers. In jest.config.js you can add line (but it not works for me):
module.exports = {
// many of lines omited
timers: 'legacy'
};
Configure legacy fake timers for individually test suite, or even test:
jest.useFakeTimers('legacy');
describe('My awesome logic', () => {
// blah blah blah
});
It's preferably to use new syntax based on #sinonjs/fake-timers. But I can't find working example for Jest, so I'll update this answer as soon as possible.
The below approach worked
beforeEach(() => {
jest.spyOn(global, 'setTimeout');
});
afterEach(() => {
global.setTimeout.mockRestore();
});
it('Test if SetTimeout is been called', {
global.setTimeout.mockImplementation((callback) => callback());
expect(global.setTimeout).toBeCalledWith(expect.any(Function), 7500);
})
In your case setTimeout is not a mock or spy, rather, it's a real function. To make it a spy, use const timeoutSpy = jest.spyOn(window, 'setTimeout'). And use timeoutSpy in the assertion.
You could also test not the fact of calling the setTimeout function, but assert that setIsPopupActive was called once, and with false. For this you might need to do jest.runOnlyPendingTimers() or jest.runAllTimers()

Jest: Mock a _HOC_ or _curried_ function

Given the following function:
./http.js
const http = {
refetch() {
return (component) => component;
}
}
I would like to mock the function in a test as follows:
./__tests__/someTest.js
import { refetch } from './http';
jest.mock('./http', () => {
return {
refetch: jest.fn();
}
}
refetch.mockImplementation((component) => {
// doing some stuff
})
But I'm receiving the error
TypeError: _http.refetch.mockImplementation is not a function
How can I mock the refetch function in the given example?
update:
When I modify the mock function slightly to:
jest.mock(
'../http',
() => ({ refetch: jest.fn() }),
);
I get a different error:
TypeError: (0 , _http.refetch)(...) is not a function
My guess it's something with the syntax where the curried function (or HOC function) is not mapped properly. But I don't know how to solve it.
Some of the real code I'm trying to test.
Note: The example is a bit sloppy. It works in the application. The example given is to give an idea of the workings.
./SettingsContainer
// ...some code
return (
<FormComponent
settingsFetch={settingsFetch}
settingsPutResponse={settingsPutResponse}
/>
);
}
const ConnectedSettingsContainer = refetch(
({
match: { params: { someId } },
}) => ({
settingsFetch: {
url: 'https://some-url.com/api/v1/f',
},
settingsPut: (data) => ({
settingsPutResponse: {
url: 'https://some-url.com/api/v1/p',
}
}),
}),
)(SettingsContainer);
export default ConnectedSettingsContainer;
Then in my component I am getting the settingsPutResponse via the props which react-refetch does.
I want to test if the user can re-submit a form after the server has responded once or twice with a 500 until a 204 is given back.
./FormComponent
// ...code
const FormComp = ({ settingsResponse }) => {
const [success, setSuccess] = useState(false);
useEffect(() => {
if (settingsResponse && settingsResponse.fulfilled) {
setSuccess(true);
}
}, [settingsResponse]);
if (success) {
// state of the form wil be reset
}
return (
<form>
<label htmlFor"username">
<input type="text" id="username" />
<button type="submit">Save</button>
</form>
)
};
The first question to ask yourself about mocking is "do I really need to mock this?" The most straightforward solution here is to test "component" directly instead of trying to fake out an http HOC wrapper around it.
I generally avoid trying to unit test things related to I/O. Those things are best handled with functional or integration tests. You can accomplish that by making sure that, given same props, component always renders the same output. Then, it becomes trivial to unit test component with no mocks required.
Then use functional and/or integration tests to ensure that the actual http I/O happens correctly
To more directly answer you question though, jest.fn is not a component, but React is expecting one. If you want the mock to work, you must give it a real component.
Your sample code here doesn't make sense because every part of your example is fake code. Which real code are you trying to test? I've seen gigantic test files that never actually exercize any real code - they were just testing an elaborate system of mocks. Be careful not to fall into that trap.

Difference between resetAllMocks, resetModules, resetModuleRegistry, restoreAllMocks in Jest

I'm trying to wrap my head around the following in Jest:
resetAllMocks, resetModules, resetModuleRegistry and restoreAllMocks
and I'm finding it difficult.
I read the jest documentation but it's not too clear. I would appreciate it if someone can please provide me with an example of how the above work and they are different from each other.
The following sections explain the behaviors of each function and its corresponding config directive. In the case of config directives, the explained behavior takes place in between each test making them more and more isolated from the other tests.
References to fn are implying a sample jest mock function under each of these actions.
jest.clearAllMocks() and clearMocks:[boolean]
Resets all the mocks usage data, not their implementation. In other words, it only replaces fn.mock.calls and fn.mock.instances properties of a jest mock function.
jest.resetAllMocks() and the resetMocks:[boolean]
A superset of clearAllMocks() which also takes care of resetting the implementation to a no return function. In other words, it will replace the mock function with a new jest.fn(), not just its fn.mock.calls and fn.mock.instances.
jest.restoreAllMocks() and restoreMocks:[boolean]
Similar to resetAllMocks(), with one very important difference. It restores the original implementation of "spies". So, it goes like "replace mocks with jest.fn(), but replace spies with their original implementation".
So, in cases where we manually assign things with jest.fn() (not spies), we have to take care of implementation restoration ourselves as jest won't be doing it.
jest.resetModules() and resetModules:[boolean]
It resets Jest's module registry which is a cache for all required/imported modules. Jest will re-import any required module after a call to this. Imagine a clean slate without having to deal with all the mocked out modules in other tests.
jest.resetModuleRegistry
It's just an alias for resetModules, see:
https://github.com/facebook/jest/blob/7f69176c/packages/jest-runtime/src/index.ts#L1147
See how clearing, resetting and restoring differ in action:
https://repl.it/#sepehr/jest-mock-api-reset-restore#jest-mock-apis.test.js
PASS ./jest-mock-apis.test.js
jest mock reset/restore api
when calling mockReset() on a test double with custom impl.
if the test double is a spy
✓ jest replaces the impl. to a new undefined-returning jest.fn() (18ms)
if the test double is "not" a spy
✓ jest replaces the impl. to a new undefined-returning jest.fn() (17ms)
when calling mockRestore() on a test double with custom impl.
if the test double is "not" a spy
✓ jest resets the impl. to a new undefined-returning jest.fn() (2ms)
if the test double is a spy
✓ jest restores the original impl. of that spy (7ms)
describe('jest mock reset/restore api', () => {
describe('when calling mockReset() on a test double with custom impl.', () => {
describe('if the test double is a spy', () => {
test('jest replaces the impl. to a new undefined-returning jest.fn()', () => {
const module = { api: () => 'actual' }
jest.spyOn(module, 'api').mockImplementation(() => 'spy mocked')
expect(module.api()).toStrictEqual('spy mocked')
expect(module.api).toHaveBeenCalledTimes(1)
module.api.mockReset()
expect(module.api()).toStrictEqual(undefined)
expect(module.api).toHaveBeenCalledTimes(1)
})
})
describe('if the test double is "not" a spy', () => {
test('jest replaces the impl. to a new undefined-returning jest.fn()', () => {
const api = jest.fn(() => 'non-spy mocked')
expect(api()).toStrictEqual('non-spy mocked')
expect(api).toHaveBeenCalledTimes(1)
api.mockReset()
expect(api()).toStrictEqual(undefined)
expect(api).toHaveBeenCalledTimes(1)
})
})
})
describe('when calling mockRestore() on a test double with custom impl.', () => {
describe('if the test double is "not" a spy', () => {
test('jest resets the impl. to a new undefined-returning jest.fn()', () => {
const api = jest.fn(() => 'non-spy mocked')
expect(api()).toStrictEqual('non-spy mocked')
expect(api).toHaveBeenCalledTimes(1)
api.mockRestore()
expect(api()).toStrictEqual(undefined)
expect(api).toHaveBeenCalledTimes(1)
})
})
describe('if the test double is a spy', () => {
test('jest restores the original impl. of that spy', () => {
const module = { api: () => 'actual' }
jest.spyOn(module, 'api').mockImplementation(() => 'spy mocked')
expect(module.api()).toStrictEqual('spy mocked')
expect(module.api).toHaveBeenCalledTimes(1)
module.api.mockRestore()
expect(module.api()).toStrictEqual('actual')
expect(module.api).not.toHaveProperty('mock')
})
})
})
})
Thanks for #sepehr answer.
I think it would be easier to understand by example.
Quick tips:
If you want to test mock function called times, clear before you use
If you want to make sure mock return value wouldn't pollute other test case, call reset
If you want to use origin method instead of mock implementation, call restore.
import {Calculator} from './calculator';
describe('calculator add', function () {
let calculator = new Calculator();
const mockAdd = jest.spyOn(calculator, 'add');
it('mock the add method', function () {
calculator.add = mockAdd.mockReturnValue(5);
expect(calculator.add(1, 2)).toBe(5);
});
it('because we didnt clear mock, the call times is 2', function () {
expect(calculator.add(1, 2)).toBe(5);
expect(mockAdd).toHaveBeenCalledTimes(2);
});
it('After clear, now call times should be 1', function () {
jest.clearAllMocks();
expect(calculator.add(1, 2)).toBe(5);
expect(mockAdd).toHaveBeenCalledTimes(1);
});
it('we reset mock, it means the mock has no return. The value would be undefined', function () {
jest.resetAllMocks();
expect(calculator.add(1, 2)).toBe(undefined);
});
it('we restore the mock to original method, so it should work as normal add.', function () {
jest.restoreAllMocks();
expect(calculator.add(1, 2)).toBe(3);
});
});

jest.mock is not a function in react?

Could you please tell me how to test componentDidMount function using enzyme.I am fetching data from the server in componentDidMount which work perfectly.Now I want to test this function.
here is my code
https://codesandbox.io/s/oq7kwzrnj5
componentDidMount(){
axios
.get('https://*******/getlist')
.then(res => {
this.setState({
items : res.data.data
})
})
.catch(err => console.log(err))
}
I try like this
it("check ajax call", () => {
const componentDidMountSpy = jest.spyOn(List.prototype, 'componentDidMount');
const wrapper = shallow(<List />);
});
see updated code
https://codesandbox.io/s/oq7kwzrnj5
it("check ajax call", () => {
jest.mock('axios', () => {
const exampleArticles:any = {
data :{
data:['A','B','c']
}
}
return {
get: jest.fn(() => Promise.resolve(exampleArticles)),
};
});
expect(axios.get).toHaveBeenCalled();
});
error
You look like you're almost there. Just add the expect():
expect(componentDidMountSpy).toHaveBeenCalled();
If you need to check if it was called multiple times, you can use toHaveBeenCalledTimes(count).
Also, be sure to mockRestore() the mock at the end to make it unmocked for other tests.
List.prototype.componentDidMount.restore();
To mock axios (or any node_modules package), create a folder named __mocks__ in the same directory as node_modules, like:
--- project root
|-- node_modules
|-- __mocks__
Inside of there, make a file named <package_name>.js (so axios.js).
Inside of there, you'll create your mocked version.
If you just need to mock .get(), it can be as simple as:
export default { get: jest.fn() }
Then in your code, near the top (after imports), add:
import axios from 'axios';
jest.mock('axios');
In your test, add a call to axios.get.mockImplementation() to specify what it'll return:
axios.get.mockImplementation(() => Promise.resolve({ data: { data: [1, 2, 3] } });
This will then make axios.get() return whatever you gave it (in this case, a Promise that resolves to that object).
You can then do whatever tests you need to do.
Finally, end the test with:
axios.get.mockReset();
to reset it to it's default mocked implementation.

jest support for mocking javascript 'classes'

I'm on a project that uses jest as the testing framework for a nodejs server. On one controller (called ControllerImTesting), it news up an instance of a helper class (we'll call ClassIWantToMock and uses it for the controller. I already have many tests written around ClassIWantToMock itself so naturally when I test ControllerImTesting, I just want to mock ClassIWantToMock out.
It's pretty simple, I've created another js file in __mocks__ that contains a dumb version of ClassIWantToMock. When I new it up in ControllerImTesting, I want it to use the dumb version in __mocks__.
I've tried lots of configurations at this point but my desperation move is to use setMock like:
jest.setMock('/path/to/real/class/I/want/to/mock', '/path/to/dumb/version') How could this fail?
But when running the test, I get TypeError: ClassIWantToMock is not a constructor.
I look at my dumb version of ClassIWantToMock and change the style from something like const ClassIWantToMock = (req) => { to class ClassIWantToMock {. I get the same error which kind of makes sense because using the es6 class style is just syntactic sugar.
Just to confirm, in my real controller, I write a line, console.log(ClassIWantToMock) above the line where it news up the instance. It indeed prints out the '/path/to/dumb/version'. It is trying to mock it but cannot.
Is this a limitation of Jest? Or am I simply not using it correctly? --> How should this be done?
UPDATE
./ClassIWantToMock.js
class ClassIWantToMock {
constructor(stuff) {
this.stuff = stuff
}
doStuff() {
console.log('some real stuff')
}
}
module.exports = ClassIWantToMock
./__mocks__/ClassIWantToMock.js
class ClassIWantToMock {
constructor(fakeStuff) {
this.fakeStuff = fakeStuff
}
doStuff() {
console.log('some fake stuff')
}
}
module.exports = ClassIWantToMock
./ControllerImTesting.js
const ClassIWantToMock = require('./ClassIWantToMock')
class ControllerImTesting {
static aMethod(req, res, next) {
const helper = ClassIWantToMock('real stuff')
helper.doStuff()
return next()
}
}
module.exports = ClassIWantToMock
./ControllerImTesting.spec.js
jest.setMock('./ClassIWantToMock', './__mocks__/ClassIWantToMock')
const ControllerImTesting = require('./ControllerImTesting')
describe('basic test', () => {
test('should work', () => {
return ControllerImTesting.aMethod({}, {}, () => {}).then(() => {
// expect console to display 'some fake stuff'
})
})
})

Categories