Testing Sentry with Jest - reactjs

I am testing my error boundaries to React and noticed in Codecov that there is a particular part of my Sentry function that hasn't been tested.
I have tried to use jest.mock("#sentry/browser") and mocking Sentry, however, can't seem to get the lines tested. The Sentry import is mocking correctly but not scope.
Here is an example of my attempt at mocking.
import * as Sentry from "#sentry/browser"
const mock_scope = jest.fn(() => {
return { setExtras: null }
})
Sentry.withScope = jest.fn().mockImplementation(mock_scope)

The untested lines are this callback function being passed to Sentry.withScope:
scope => {
scope.setExtras(errorInfo);
Sentry.captureException(error);
}
Since Sentry.withScope has been mocked you can use mockFn.mock.calls to retrieve the callback function passed to it.
Once you have retrieved the callback function, you can call it directly to test it.
Here is a slightly simplified working example:
import * as Sentry from '#sentry/browser';
jest.mock('#sentry/browser'); // <= auto-mock #sentry/browser
const componentDidCatch = (error, errorInfo) => {
Sentry.withScope(scope => {
scope.setExtras(errorInfo);
Sentry.captureException(error);
});
};
test('componentDidCatch', () => {
componentDidCatch('the error', 'the error info');
const callback = Sentry.withScope.mock.calls[0][0]; // <= get the callback passed to Sentry.withScope
const scope = { setExtras: jest.fn() };
callback(scope); // <= call the callback
expect(scope.setExtras).toHaveBeenCalledWith('the error info'); // Success!
expect(Sentry.captureException).toHaveBeenCalledWith('the error'); // Success!
});
Note that this line:
const callback = Sentry.withScope.mock.calls[0][0];
...is getting the first argument of the first call to Sentry.withScope, which is the callback function.

An addition to the accepted answer. The solution there requires to manually invoke the callback (see the callback(scope); // <= call the callback line in the test code).
Here is how to make it work automatically:
import * as Sentry from '#sentry/browser'
jest.mock('#sentry/browser')
// Update the default mock implementation for `withScope` to invoke the callback
const SentryMockScope = { setExtras: jest.fn() }
Sentry.withScope.mockImplementation((callback) => {
callback(SentryMockScope)
})
And then the test code becomes:
test('componentDidCatch', () => {
componentDidCatch('the error', 'the error info');
expect(SentryMockScope.setExtras).toHaveBeenCalledWith('the error info');
expect(Sentry.captureException).toHaveBeenCalledWith('the error');
});

Related

jest - react - mocked geolocation inside useEffect in hook

how would you test a hook that is using navigator.geolocation.getCurrentPosition() method (mocked it already based on this: https://newdevzone.com/posts/how-to-mock-navigatorgeolocation-in-a-react-jest-test) in useEffect?
I can't see the way how to wait for result as upon calling the function it does not return anything and returned data are processed in passed callback. found some hackish solution that works when run debug using jest runner vsc extension, but does not for regular test run.
the hook:
const useGeolocation = () => {
const [coords, setCoords] = useState<GeolocationCoordinates>();
useEffect(() => {
navigator.geolocation.getCurrentPosition((position) => {
setCoords(position.coords);
});
}, []);
return coords;
};
test:
import { renderHook } from '#testing-library/react';
it('should return non-empty coords', async () => {
const { result } = renderHook(() => useGeolocation());
// MISSING CODE
expect(result.current).not.toBe(undefined);
});
I'll be happy for any idea leading to solution so I don't need to give up on unit testing once again :slight_smile:

Jest - how to mock a class in jest

I've been trying to mock a test in jest through the methods that they have on their documentation. By mocking the whole class but I can't seem to get it to work properly.
https://jestjs.io/docs/en/es6-class-mocks
jest.mock('../../../../../src/SubscriptionOrder');
SubscriptionOrder.prototype.createChargebeeSubscription = jest.fn(() => 'response');
const test = new SubscriptionOrder(
'subscription',
[{}],
'errorMethods',
'customerMethods',
);
test.createChargebeeSubscription();
I'd expect this to mock the createChargebeeSubscription method and return the string response but it seems to be returning undeifined
Then this is the piece of code I'm trying to run a test for as well.
const subscriptionOrder = new SubscriptionOrder(
'subscription',
subscriptionRequest,
errorMethods,
customerMethods,
);
const response = await subscriptionOrder.createChargebeeSubscription(token);
this.setState({ successfulSubmit: response });
I want to update the state to the string response but getting undefined instead. so it appears I'm kinda mocking something but just not properly.
You can use spyOn as follows to do the mocking for you. I also recommend that you set up and tear down this spy once you are finished.
So here's a sample piece of code which will do what you want:
describe('createChargebeeSubscription() method behaviour', () => {
let createChargebeeSubscriptionSpy;
let testResponse;
beforeAll(() => {
// Lets create an instance of your class first
const subscriptionOrder = new SubscriptionOrder(
'subscription',
subscriptionRequest,
errorMethods,
customerMethods
);
// Now use a spy to mock the return value from your function
createChargebeeSubscriptionSpy = jest.spyOn(subscriptionOrder, 'createChargebeeSubscription').mockImplementation(() => {
return 'response';
});
// Finally invoke the method being tested
testResponse = subscriptionOrder.createChargebeeSubscription();
});
afterAll(() => {
// Restore the functionality (ie. disable the spy) of your method
createChargebeeSubscriptionSpy.mockRestore();
});
it('verifies that the expected response was returned', () => {
expect(testResponse).toBe('response');
});
});

Testing typescript asyc functions, Async callback was not invoked even though it's called in .then() method

I've been searching around for a while for a resolution to this but can't find anything, the only thing I can think of is redux-mock-store doesn't support Promises.
I have this test method below.
import thunk from 'redux-thunk';
import configureMockStore from 'redux-mock-store';
import { cleanAll } from 'nock';
var middleware = [thunk];
var mockStore = configureMockStore(middleware);
describe('Organisation thunk actions', () => {
afterAll(() => {
cleanAll();
});
describe('getOrganisation', () => {
test('should create BEGIN_GET_ORGANISATION_AJAX action when getting organsation', (done: any) => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000;
var expectedAction = [{ type: types.BEGIN_GET_ORGANISATION_AJAX }];
var store = mockStore({});
store.dispatch<any>((OrganisationActions.getOrganisation("e"))).then(() => {
var actions = store.getActions();
expect(actions).toEqual(expectedAction);
done();
});
});
});
});
This is designed to test the action below.
export const getOrganisation = (organisationId: string, expands?: string[]) => {
return (dispatch: any) => {
dispatch(beginGetOrganisationAjax());
return OrganisationApi.getOrganisationAsync(organisationId).then((organisation: any) => {
dispatch(getOrganisationSuccess(organisation))
}).catch(error => {
throw (error);
});
}
}
Where OrganisationApi.getOrganisationAsync(organisationId) is a call to a mockApi that I know works from usage.
When I run this test it fails twice after the 30s DEFAULT_TIMEOUT_INTERVAL specified, once as expected (as I've designed the expect to) but the second failure is an error "Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.".
Unless I'm mistaken, the async callback is the done() function invoked after the expect(actions).toEqual(expectedAction) within the .then() function in the test.
As the expect fails the .then() is definitely running but doesn't seem to be running the .done() function, can anyone see why this might be happening?
Finally figured out the issue, posting the resolution in case someone has a similar problem.
The Async callback is never called as the test function quits out as soon as it comes across the failing expect() statement, therefore it never actually runs the done() function in this instance.
Once this test passes it calls the done() function and my tests pass.

Jest mock a function within object

Trying the following :
// Function Spies
const onSubmitSpy = jest.fn().mockName('onSubmitSpy');
const onHistoryPushSpy = jest.fn().mockName('onPushSpy');
// Default Props
const defaultProps = {
signupUserMutation: onSubmitSpy,
history: {
push: onHistoryPushSpy
}
};
Then within my test this spy gets called withing my code like this
history.push('/');
(I verified with mock Response) but the call count in test response always remains 0.
I have a feeling this is maybe because this is a nested object I am missing something ?
test('should submit and call routing change', () => {
...
expect(onHistoryPushSpy).toHaveBeenCalledTimes(1);
});
And I always get this error
Expected mock function to have been called one time, but it was called zero times.

Return value of a mocked function does not have `then` property

I have the following async call in one of my React components:
onSubmit = (data) => {
this.props.startAddPost(data)
.then(() => {
this.props.history.push('/');
});
};
The goal here is to redirect the user to the index page only once the post has been persisted in Redux (startAddPost is an async action generator that sends the data to an external API using axios and dispatches another action that will save the new post in Redux store; the whole thing is returned, so that I can chain a then call to it in the component itself). It works in the app just fine, but I'm having trouble testing it.
import React from 'react';
import { shallow } from 'enzyme';
import { AddPost } from '../../components/AddPost';
import posts from '../fixtures/posts';
let startAddPost, history, wrapper;
beforeEach(() => {
startAddPost = jest.fn();
history = { push: jest.fn() };
wrapper = shallow(<AddPost startAddPost={startAddPost} history={history} />);
});
test('handles the onSubmit call correctly', () => {
wrapper.find('PostForm').prop('onSubmit')(posts[0]);
expect(startAddPost).toHaveBeenLastCalledWith(posts[0]);
expect(history.push).toHaveBeenLastCalledWith('/');
});
So I obviously need this test to pass, but it fails with the following output:
● handles the onSubmit call correctly
TypeError: Cannot read property 'then' of undefined
at AddPost._this.onSubmit (src/components/AddPost.js:9:37)
at Object.<anonymous> (src/tests/components/AddPost.test.js:25:46)
at process._tickCallback (internal/process/next_tick.js:109:7)
So how can I fix this? I suspect this is a problem with the test itself because everything works well in the actual app. Thank you!
Your code is not testable in the first place. You pass in a callback to the action and execute it after saving the data to the database like so,
export function createPost(values, callback) {
const request = axios.post('http://localhost:8080/api/posts', values)
.then(() => callback());
return {
type: CREATE_POST,
payload: request
};
}
The callback should be responsible for the above redirection in this case. The client code which uses the action should be like this.
onSubmit(values) {
this.props.createPost(values, () => {
this.props.history.push('/');
});
}
This makes your action much more flexible and reusable too.
Then when you test it, you can pass a stub to the action, and verify whether it is called once. Writing a quality, testable code is an art though.
The problem with your code is that the startAddPost function is a mock function which does not return a Promise, but your actual this.props.startAddPost function does return a Promise.
That's why your code works but fails when you try to test it, leading to the cannot read property.... error.
To fix this make your mocked function return a Promise like so -
beforeEach(() => {
startAddPost = jest.fn().mockReturnValueOnce(Promise.resolve())
...
});
Read more about mockReturnValueOnce here.

Resources