My simple understanding of these two functions in react/frontend projects is the following:
jest.fn()
- You want to mock a function and really don't care about the original implementation of that function (it will be overridden by
jest.fn()) - Often you just mock the return value
- This is very helpful if you want to remove dependencies to the backend (e.g. when calling backend API) or third party libraries in your tests
- It is also extremely helpful if you want to make real unit tests. You don't care about if certain function that gets called by the unit you test is working properly, because thats not part of it's responsibility.
jest.spyOn()
- The original implementation of the function is relevant for your test, but:
- You want to add your own implementation just for a specific scenario and then reset it again via
mockRestore()(if you just use ajest.spyOn()without mocking it further it will still call the original function by default) - You just want to see if the function was called
- ...
- You want to add your own implementation just for a specific scenario and then reset it again via
- I think this is especially helpful for integration tests, but not only for them!
(Good blog post: https://medium.com/@rickhanlonii/understanding-jest-mocks-f0046c68e53c)
Answer from ysfaran on Stack OverflowMy simple understanding of these two functions in react/frontend projects is the following:
jest.fn()
- You want to mock a function and really don't care about the original implementation of that function (it will be overridden by
jest.fn()) - Often you just mock the return value
- This is very helpful if you want to remove dependencies to the backend (e.g. when calling backend API) or third party libraries in your tests
- It is also extremely helpful if you want to make real unit tests. You don't care about if certain function that gets called by the unit you test is working properly, because thats not part of it's responsibility.
jest.spyOn()
- The original implementation of the function is relevant for your test, but:
- You want to add your own implementation just for a specific scenario and then reset it again via
mockRestore()(if you just use ajest.spyOn()without mocking it further it will still call the original function by default) - You just want to see if the function was called
- ...
- You want to add your own implementation just for a specific scenario and then reset it again via
- I think this is especially helpful for integration tests, but not only for them!
(Good blog post: https://medium.com/@rickhanlonii/understanding-jest-mocks-f0046c68e53c)
To my understanding the only difference is that YOU CAN RESTORE ORIGINAL FUNCTION with jest.spyOn and you can't with jest.fn.
Imagine we have some hook that calls a function when component is rendered, here we can just check the function was called, we do not test that function.
Another case if we want original function to test how it works. And we need both in one test file.
Real method:
myMethod() {
return 33;
}
With jest.fn()
const myMethod = jest.fn().mockImplementation(() => 25);
const result = myMethod();
expect(result).toBe(25);
In case we want to test now real myMethod, we can't restore it back to normal with jest.fn().
Another thing with spies:
const spy_myMethod = jest.spyOn(component, "myMethod").mockImplementation(() => 25);
const result = myMethod();
expect(result).toBe(25);
And now if we want the original myMethod
spy_myMethod.mockRestore();
const result = myMethod();
expect(result).toBe(33);
Jest.mock vs jest.spyOn
How to use jest.spyOn with @swc/jest?
How do I spyOn third party function with jest?
Jest spyOn a function not Class or Object type
Videos
I'm still kind of confused when to uese each implementation. Like i've been looking only and to what I understand is if you want a dummy implementation and don't care about ever getting the original values then use jest.mock. If you want to validate that a function is called then use jest.SpyOn
Would everyone agree with this?
The example code mixes ES6 import/ export syntax with Node module.exports syntax...
...but based on a library that looks like this:
lib.js
function Viewer() { }
Viewer.prototype.foo = function () { }
module.exports = Viewer;
...it would be used like this:
mp_wrapper.js
import Viewer from './lib'; // <= Babel allows Viewer to be used like an ES6 default export
export const createViewer = container => new Viewer(container);
...and to spy on Viewer you would need to mock the entire library in your test:
mp_wrapper.spec.js
import Viewer from './lib';
import { createViewer } from './mp_wrapper';
jest.mock('./lib', () => jest.fn()); // <= mock the library
test('returns a new instance of the Viewer class', () => {
const viewer = createViewer('the container');
expect(Viewer).toHaveBeenCalledWith('the container'); // Success!
expect(viewer).toBeInstanceOf(Viewer); // Success!
});
Note that if the library was an ES6 library then you could spy on the default export directly like this:
import * as lib from './lib';
const spy = jest.spyOn(lib, 'default'); // <= spy on the default export
...but because of the way Babel handles the interop between ES6 and non-ES6 code this approach doesn't work if the library is not ES6.
Edit: response to the follow-up question
jest.genMockFromModule generates a mocked version of the module and returns it.
So for example this:
const mock = jest.genMockFromModule('lib');
...generates a mocked version of lib and assigns it to mock. Note that this does not mean that the mock will be returned when lib is required during a test.
jest.genMockFromModule can be useful when creating a manual mock:
__mocks__/lib.js
const lib = jest.genMockFromModule('lib'); // <= generate a mock of the module
lib.someFunc.mockReturnValue('some value'); // <= modify it
module.exports = lib; // <= export the modified mock
In your final solution you have these two lines:
jest.genMockFromModule('lib');
jest.mock('lib');
This line:
jest.genMockFromModule('lib');
...doesn't actually do anything since it is generating a mock of the module but the returned mock isn't being used for anything.
This line:
jest.mock('lib');
...tells Jest to auto-mock the lib module and is the only line that is needed in this case.
Here is a solution:
util.js
const util = {
isElement() {}
};
module.exports = util;
View.js, the third-party module:
function Viewer() {
// Doing things
console.log('new viewer instance');
}
Viewer.prototype.foo = function() {};
module.exports = { Viewer };
my_wrapper.js:
const { Viewer } = require('./viewer');
const util = require('./util');
module.exports = {
createViewer: container => {
if (util.isElement(container)) {
return new Viewer(container);
} else {
throw new Error('Invalid Element when attempting to create underlying viewer.');
}
}
};
Unit test:
const { Viewer } = require('./viewer');
const my_wrapper = require('./');
const util = require('./util');
jest.mock('./viewer', () => {
return {
Viewer: jest.fn()
};
});
describe('mp_wrapper', () => {
beforeEach(() => {
jest.resetAllMocks();
});
describe('createViewer', () => {
it('t1', () => {
util.isElement = jest.fn().mockReturnValueOnce(true);
let viewer = my_wrapper.createViewer('el');
expect(util.isElement).toBeCalledWith('el');
expect(viewer).toBeInstanceOf(Viewer);
});
it('t2', () => {
util.isElement = jest.fn().mockReturnValueOnce(false);
expect(() => my_wrapper.createViewer('el')).toThrowError(
new Error('Invalid Element when attempting to create underlying viewer.')
);
expect(Viewer).not.toBeCalled();
});
});
});
Unit test result:
PASS src/stackoverflow/57712713/index.spec.js
mp_wrapper
createViewer
✓ t1 (6ms)
✓ t2 (5ms)
----------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 50 | 100 | |
index.js | 100 | 100 | 100 | 100 | |
util.js | 100 | 100 | 0 | 100 | |
----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 4.134s, estimated 9s
I am not expert in jest, but my recommendation to think about:
1) When the function is exported as default I use something like:
import Funct1 from "../../../src/components/Funct1";
...
jest.mock("../../../src/components/Funct1");
...
expect(Funct1).toHaveBeenCalledWith(params);
2) When the module (utils.js) has multiple exports as
export const f1 = () => {};
...
export const f8 = () => {};
You can try
import * as Utils from "../../../src/components/utils"
const f8Spy = jest.spyOn(Utils, 'f8');
...
expect(f8Spy).toHaveBeenCalledWith(params);
Similar discussion here
Wrap your function with jest.fn. Like this:
const simpleFn = (arg) => arg;
const simpleFnSpy = jest.fn(simpleFn);
simpleFnSpy(1);
expect(simpleFnSpy).toBeCalledWith(1); // Passes test