I am trying to standardise the way that regularly mocked functions get mocked.
So I have a function which handles a call to a home screen context in useHomeScreenContext.ts.
export const useHomeScreenContext = () => useContext(/* context here */)
Then the component I am testing uses that hook
Component.tsx
export const Component = () => {
const context = useHomeScreenContext();
}
Then my test file for Component looks like
Component.test.tsx
const contextSpy = jest.spyOn(context, 'useHomeScreenContext');
it("works", () => {
contextSpy.mockReturnValue(fakeValue)
expect(fakeValue).toBeTruthy()
})
The issue that I have with this approach is that there will be a large number of files that will need to be tested by mocking this context and the actual mock is a little bit more complex than I have put in here. So what I would like to do is to standardise the mock using a utility function, so I have created a separate file called mockHomeScreenContext.ts which looks a little something like this.
const contextSpy = jest.spyOn(context, 'useHomeScreenContext');
export const mockHomeScreenContext = (context) => {
beforeAll(() => {
contextSpy.mockReturnValue(mergeDeepLeft(context, homeScreenContextDefaults));
});
afterAll(() => {
contextSpy.mockRestore();
});
};
With the idea being that it is used inside of a describe block and it will tidy itself up at the end of the describe block like so
describe('and does not have funds', () => {
mockHomeScreenContext(contextOverrides);
it("works", () => {
// Tests here
})
});
And this seems to work really well, until I have 2 describe blocks and then things start to go a bit haywire and some tests seem to mock the data correctly but then all following tests will fail as the mock function is not returning anything. Getting rid of the afterAll call to clean up the mock helps but it then infects other tests.
I have been round and round in circles for days trying to get this working and I feel like it really shouldnt be this difficult and I am just missing a little bit of an understanding as to how jest mocks work.
I think the problem is in the fact that your contextSpy is global for all tests, because it's created outside of mockHomeScreenContext function. As you can find in documentation, mockRestore would restore the original (non-mocked) implementation. You might try to use mockClear instead, as it's a softer version (I usually just use that).
However, I would propose to not mock your context like this, but rather create a mock provider that would cause context to work properly, without a need to do extensive mocking (which as you probably see can be quie painful).
Here's example of what I have in mind:
Usage in test:
<MockProvider context={contextOverrides}>
<YourComponent/>
</MockProvider>
And implementation:
const MockProvider = ({context, children}) => {
return (
<YourContext.Provider value={mergeDeepLeft(context, homeScreenContextDefaults)}>
{children}
</YourContext.Provider>
)
}
This way you would get rid of all those hard mocks and would use real context (with some mocked value).
Related
Having the following NgRX effect
loadData$: Observable<any> = createEffect((): Observable<Action> => {
return this.actions$.pipe(
ofType(loadDataStarted),
switchMap(() => {
return timer(0, 10000).pipe(
switchMap(() => this.dataService.loadNewData().pipe(
)),
);
}),
);
});
after all the requirements are mocked try to test if dataService.loadNewData() is getting called, but the test will fail
beforeEach(() => {
service.loadNewData.and.returnValue(of(data));
});
it('should call service', fakeAsync(() => {
tick(10000)
expect(service.loadNewData).toHaveBeenCalledOnceWith();
}));
How do I mock correctly this case
It seems you perform the subscription to the effect outside of the actual test (the it block). This means the subscription lives outside of the fakeAsync timer and because of this, the tick(..) has no effect.
To solve this, make sure to setup everything inside your fakeAsync test block.
A general recommendation would be to use marble testing for this: https://ngrx.io/guide/effects/testing#marble-diagrams
Edit: A concrete solution is found in https://stackoverflow.com/a/65857864/2544163
I tried to call an unsubscribe function:
unsubscribeFromThingIds(arg.tids);
after the await cacheEntryRemoved; line in this example.
The function is defined like this:
export const unsubscribeFromThingIds = (tids: string[]) => {
followedThingIds = followedThingIds.filter(
id => !tids.includes(id)
);
const argument = { tids: followedThingIds } as ITimersChannelInitParams;
myIo!.emit("configure timers channel", argument);
};
(where myIo is the Socket.IO client)
This is not working currently for me. I am currently debugging this issue.
I also tried to use an useEffect hook with a returned cleanup function for running it when the component unmounts, something like this:
useEffect(() => {
return () => {
unsubscribeFromThingIds(getTimersQueryParams.tids);
};
}, [getTimersQueryParams]);
I want to ask for this information because I am not sure which one of the two ideas should I work on more, or if there is another thing I can do that is better.
Could it be that you are just impatient? The cache entry will be removed 60 seconds (the keepUnusedDataFor option) after the last component stops using it.
Generally: using the endpoint lifecycle and awaiting cacheEntryRemoved is the right way to do this if you started the listener within onCacheEntryAdded. If you want it to happen earlier, just use a shorter keepUnusedDataFor duration.
I'm trying to test the following code. I'm using jest and react testing library. This is the firs time I've used setState like this. I solved my initial which was to avoid passing in the dependency of current state but I'm not sure how can I test this. Can someone please advise.
useEffect(() => {
setUsers(currentUsers => {
if(currentUsers === undefined) {
return userDataFromApi;
} else {
//Users already exist in state
const mergedUserData = currentUsers.map(existingUser => {
const matchedUser = userDataFromApi.find(user => user.name === existingUser.name);
if (matchedUser) {
existingUser.stats = user.stats;
}
return existingUser;
});
return mergedUserData;
}
});
}, [setUsers, userDataFromApi]);
This piece of code is implementation detail. React testing library enforces UI testing. You can read this article from Kent Dodds.
In your tests you can do the same thing as the user would do (fill a form, click etc.), and then check what the user should see or not see (maybe his name, his stats etc.).
And if you get data from your backend and you would like to test only the frontend, you can mock the answer of the backend.
I have an action in my ReactJS project that calls a notification service. It is required that, if the service call fails once, I must try calling the service again only one time before proceeding with the error state in the application. I used the promise-retry module for this and was able to get it working locally. However, I am now trying to write unit tests (Mocha) for the promiseRetry-wrapped service calls themselves and having incredible difficulty getting meaningful tests to pass. First, here is the action that calls the service, wrapped in a promiseRetry.
import promiseRetry from 'promise-retry';
...
const sendNotification = () => {
return (dispatch, getState) => {
const request = buildNotificationRequest(getState);
dispatch(createNotificationAttempt());
promiseRetry((retry) => {
return createNotificationService(request)
.catch(retry);
}, {retries: 1}).then(
() => {
dispatch(createNotificationSuccess());
},
(error) => {
dispatch(createNotificationError(error));
}
);
};
};
Typically, the way that I would write unit tests for actions calling services is something like this:
describe('notification actions', () => {
beforeEach(() => {
sendNotification = sinon.stub(services, 'createNotificationService').returns(Promise.resolve({}));
});
it('should log an attempt', () => {
store.dispatch(notificationActions.sendNotification());
const actions = store.getActions();
expect(actions[0].type).to.equal(notificationActions.ACTION_TYPES.CREATE_NOTIFICATION_ATTEMPT);
});
});
This works fine for testing the initial attempt, but for some reason, even though I can debug and step through the tests and hit all of the code inside the promiseRetry, the actions inside of them (such as dispatch(createNotificationSuccess())) are not logged in the store, so I cannot run expect statements on them. Every angle I have tried up to this point only retrieves the attempt from the store, and I cannot get any data from the success or failure side of the Promise.
I have found some information on Stack Overflow about testing promise-retry itself, but I need to know that if I stub the service I'm calling and force it to fail, that it will log another attempt and another failure. Or, if I stub the service and force it to succeed, it will only log one attempt, one success, and complete. As I mentioned previously, the only action I am getting in the store is the attempt, and nothing about success or failure, even though stepping through debug shows that all of those lines of code are hit.
Here is an example of a test that I cannot get to pass:
import * as services from 'services.js';
...
describe('the first time the service call fails', () => {
const error = {status: 404};
beforeEach(() => {
sendNotification = sinon.stub(services, 'createNotificationService').returns(Promise.reject(error));
});
it('should log a retry', () => {
store.dispatch(notificationActions.sendNotification());
const actions = store.getActions();
expect(actions[0].type).to.equal(notificationActions.ACTION_TYPES.CREATE_NOTIFICATION_ATTEMPT); // this passes
expect(actions[1].type).to.equal(notificationActions.ACTION_TYPES.CREATE_NOTIFICATION_FAILURE); // this fails because there are no other actions logged in the store.
Maybe I am misunderstanding the way promise-retry works? Shouldn't it hit my error action (dispatch(createNotificationError(error)) the first time it fails, and the second time (if applicable)? If not, it should be at least logging two attempts. Any advice?
I've been developing an app in react.
Just a Simple app, it has a feature where I check toggle/toggle state for items inside a list.
At utils.js I have
export const partial = (fn, ...args) => fn.bind(null, ...args)
const _pipe = (f, g) => (...args) => g(f(...args))
export const pipe = (...fns) => fns.reduce(_pipe)
but the there is a problem in App.js, when using the utils:
const getToggledTodo = pipe(findById, toggleCompleted)
the helpers' imports seem fine:
import {pipe, partial} from './lib/utils'
import {addTodo, generateId, findById, toggleCompleted,
updateTodo, removeTodo, filterTodos} from './lib/todoHelpers'
Still , the app complains
Uncaught TypeError: Cannot read property 'find' of undefined
doing console I get:
f2: function () {
return g(f.apply(undefined, arguments));
}
I looked at:
at findById (todoHelpers.js:15)
at utils.js:10
at Object.executeOnChange (LinkedValueUtils.js:132)
and seems to me the undefined is coming from linkedValue file at last line:
executeOnChange: function (inputProps, event) {
if (inputProps.valueLink) {
_assertValueLink(inputProps);
return inputProps.valueLink.requestChange(event.target.value);
} else if (inputProps.checkedLink) {
_assertCheckedLink(inputProps);
return inputProps.checkedLink.requestChange(event.target.checked);
} else if (inputProps.onChange) {
return inputProps.onChange.call(undefined, event);
}
}
};
Not sure how .apply and .call relate to each other here, and seems to me that I'm missing an argument somewhere.
The final objective is to update of state complete/not complete in the db, plus an message in the UI saying that in fact the item has been updated.
Fun fact: if I hard code some similar structured object in App.js and use it in memory to change state, the error does not show... o_O.
It only appears when trying to connect to a 'db', which is still of course a mock. Don't know if its related but I think is worth mentioning.
Using json-server to mock db objects.
So my question is: how to debug this error? Can someone help me understand a bit how apply and call relate to this error.
Any pointers in the right direction would be very helpful and much appreciated.