Hi, perhaps I'm not understanding how Jest's spyOn works, but I'm testing my Action Creators and I'm spying on two methods that should both be called when the correct condition is met, but only one method seems to be getting called and I'm not sure if I'm doing something wrong or if there's a bug in Jest's spyOn implementation.
The method I'm testing:
export const redirectToRootTestClass = (orgId, projectId, testClassList) => {
const rootTestClassId = ProjectHelper.getBaseTemplateId(testClassList);
const redirectPath = ProjectHelper.getTestClassRedirectPath(orgId, projectId, rootTestClassId);
return function(dispatch) {
dispatch({
type: ProjectTestClassActions.REDIRECTING_TO_ROOT_TEST_CLASS
});
dispatch(replace(redirectPath));
if (rootTestClassId) { // <-- this results in a value of 123
dispatch(fetchProjectTestClass(orgId, projectId, rootTestClassId)); // spying on this method
dispatch(fetchTestClassMethodList(orgId, projectId, rootTestClassId)); // spying on this method
}
};
};
The test:
import * as actions from 'app/projects/test-classes/ProjectTestClassActions';
import * as testMethodActions from 'app/projects/test-classes/test-method/ProjectTestMethodActions';
it('should fetch the list of test classes and methods if the getBaseTemplateId() method returns a value', (done) => {
const spyOne = jest.spyOn(actions, 'fetchProjectTestClass');
const spyTwo = jest.spyOn(testMethodActions, 'fetchTestClassMethodList');
const classes = [{
id: 123,
name: 'TestClassTemplate',
isRoot: true
}];
const store = mockStore({});
store.dispatch(actions.redirectToRootTestClass(1, 2, classes));
setTimeout(() => {
expect(spyOne).toHaveBeenCalled(); // does not get called
expect(spyTwo).toHaveBeenCalled(); // does get called
done();
}, 100);
});
The result:
spyOne.mock = { calls: [], instances: [], timestamps: [] }
spyTwo.mock = { calls: [ [ 1, 2, 123 ] ], instances: [ undefined ], timestamps: [ 1515523950597 ] }
SpyOne and SpyTwo should both be getting called with the same inputs, but only the second one is getting called for some reason. Would love some insight as to why this is happening.
Using Jest version 22.0.4
@goodbomb It's very difficult to see real issue, unless implementation for testMethodActions & actions is not provided. Can you create a mini repo which can reproduce this issue?
Hi @anilreddykatta. Here are the methods being called from testMethodActions and actions. I'll see if I can put together a working example in a bit.
testMethodActions
/**
* Sends a GET request to retrieve a specific set of test methods.
*
* @param {object} response [The response from the API.]
* @return {object} [The FETCH_TEST_METHOD_LIST_SUCCESS action.]
*/
export const _fetchTestClassMethodListSuccess = (response) => {
return {
type: ProjectTestMethodActions.FETCH_TEST_METHOD_LIST_SUCCESS,
payload: response
};
};
/**
* Fetches the list of test methods for a test class.
*
* @param {number} orgId [The ID of the org.]
* @param {number} projectId [The ID of the project to fetch.]
* @param {number} testClassId [The ID of the test class to fetch.]
* @return {function} [The successful request action along with the data from the request.]
*/
export const fetchTestClassMethodList = (orgId, projectId, testClassId) => {
const request = ProjectTestMethodService.fetchTestClassMethodList(orgId, projectId, testClassId);
return function(dispatch) {
dispatch(sendRequest(ProjectTestMethodActions.SEND_REQUEST));
return request.then(
data => dispatch(_fetchTestClassMethodListSuccess(data)),
error => dispatch(requestFailed(error, ProjectTestMethodActions.REQUEST_FAIL))
);
};
};
actions
/**
* Sends a GET request to retrieve a specific test class.
*
* @param {object} response [The response from the API.]
* @return {object} [The FETCH_TEST_CLASS_SUCCESS action.]
*/
export const _fetchProjectTestClassSuccess = (response) => {
return {
type: ProjectTestClassActions.FETCH_TEST_CLASS_SUCCESS,
payload: response
};
};
/**
* Fetches a specific project test class.
*
* @param {number} orgId [The ID of the org.]
* @param {number} projectId [The ID of the project to fetch.]
* @param {number} testClassId [The ID of the test class to fetch.]
* @return {function} [The successful request action along with the data from the request.]
*/
export const fetchProjectTestClass = (orgId, projectId, testClassId) => {
const request = ProjectTestClassService.fetchProjectTestClass(orgId, projectId, testClassId);
return function(dispatch) {
dispatch(sendRequest(ProjectTestClassActions.SEND_REQUEST));
return request.then(
data => dispatch(_fetchProjectTestClassSuccess(data)),
error => dispatch(requestFailedRedirect(error, ProjectTestClassActions.REQUEST_FAIL))
);
};
};
/**
* Redirects to the Root Test Template when loading the base models view.
*
* @param {number} orgId [The ID of the org.]
* @param {number} projectId [The Id of the project.]
* @param {array} testClassList [The list of models to parse.]
* @return {function} [Dispatches a route change.]
*/
export const _redirectToRootTestClass = (orgId, projectId, testClassList) => {
const rootTestClassId = ProjectHelper.getBaseTemplateId(testClassList);
const redirectPath = ProjectHelper.getTestClassRedirectPath(orgId, projectId, rootTestClassId);
return function(dispatch) {
dispatch({
type: ProjectTestClassActions.REDIRECTING_TO_ROOT_TEST_CLASS
});
dispatch(replace(redirectPath));
if (rootTestClassId) {
dispatch(fetchProjectTestClass(orgId, projectId, rootTestClassId)); // does NOT get called in the test
dispatch(fetchTestClassMethodList(orgId, projectId, rootTestClassId)); // does get called in the test
}
};
};
Sorry to jump on your thread @goodbomb - I'm also experiencing weird behaviour with spyOn.
Shorter code examples with all details.
Class to test:
import { EventEmitter } from 'events';
class MyClass {
constructor() {
this._onSomeEvent = this._onSomeEvent.bind(this);
this._emitter = new EventEmitter();
this._emitter.on('SOME_EVENT', this._onSomeEvent);
}
_onSomeEvent() {
console.log('[MyClass] _onSomeEvent');
}
}
export default MyClass;
Test:
import MyClass from '../src/MyClass';
describe('MyClass', () => {
it('should call the SOME_EVENT listener', () => {
const instance = new MyClass();
const spy = jest.spyOn(instance, '_onSomeEvent');
instance._emitter.emit('SOME_EVENT');
expect(spy).toHaveBeenCalled();
});
});
toHaveBeenCalled fails but the log from MyClass::_onSomeEvent is outputted to the console.
I think this should be
import MyClass from '../src/MyClass';
describe('MyClass', () => {
it('should call the SOME_EVENT listener', () => {
const spy = jest.spyOn(MyClass.prototype, '_onSomeEvent');
const instance = new MyClass();
instance._emitter.emit('SOME_EVENT');
expect(spy).toHaveBeenCalled();
});
});
Hey all, going to close as I don't think there's anything here demonstrating a bug
If you need help using spyOn, we recommend using our discord channel or StackOverflow where there is an active jestjs tag for questions
Thanks @ramiel
Only issue I see with that is if you have some code that creates multiple instances, but you only want to spy on the listener of one of those instances.
Simple example:
it('should call the SOME_EVENT listener', () => {
const spy = jest.spyOn(MyClass.prototype, '_onSomeEvent');
const instance = new MyClass();
instance._emitter.emit('SOME_EVENT');
const otherInstance = new MyClass();
otherInstance._emitter.emit('SOME_EVENT');
expect(spy.mock.calls.length).toEqual(1);
});
The test fails as the spy gets called twice - which actually isn't a bug, it's what you'd expect from spying on the prototype. But it unfortunately isn't a workaround for spying on a listener in a class instance.
The test fails because its specification is wrong. If you call emit two times you should expect to have it called two times. By the way I understand your problem, you want to know it per instance. In the code you just wrote you can write
it('should call the SOME_EVENT listener', () => {
const instance = new MyClass();
instance._emitter.emit('SOME_EVENT');
const otherInstance = new MyClass();
const spy = jest.spyOn(otherInstance._emitter, 'emit');
otherInstance._emitter.emit('SOME_EVENT');
expect(spy.mock.calls.length).toEqual(1);
});
but in your original code you called emit in the constructor which make this test impossible to write like that
Most helpful comment
Sorry to jump on your thread @goodbomb - I'm also experiencing weird behaviour with spyOn.
Shorter code examples with all details.
Class to test:
Test:
toHaveBeenCalledfails but the log fromMyClass::_onSomeEventis outputted to the console.