redux-saga-test-plan
redux-saga-test-plan copied to clipboard
Test delay from redux-saga
Background
I have a "gradual-backoff" built into a saga using Redux-saga's delay() effect.
I am using expectSaga to write the test.
But in order to get the test to work I had to disable the delay.
Question
What is the best way to test the delay amount?
Code
"Back off" function that returns 1s, 2s, or 3s
const BACK_OFF_SERIES = [1000, 2000, 3000];
const backOff = attempt =>
BACK_OFF_SERIES[Math.min(attempt, BACK_OFF_SERIES.length - 1)];
tryDownload saga
const MAX_ATTEMPTS = 3;
function* tryDownloadFiles(assets, downloadOptions) {
for (let attempts = MAX_ATTEMPTS; attempts; attempts -= 1) {
try {
const urls = yield call(downloader, assets, downloadOptions);
return urls;
} catch (error) {
if (attempts > 1) {
yield call(delay, backOff(attempts));
} else {
yield put(
showError({
error: `${error}. Failed!`,
})
);
}
}
}
}
And an expectSaga test that uses a provider bypass the delay:
const res = expectSaga(tryDownloadFiles, mockAssets, mockDownloadOptions)
.provide([
[matchers.call.fn(delay), null],
[matchers.call.fn(downloader), throwError(mockError)],
])
// .call(delay, backOff(3))
// .call(delay, backOff(2))
// .call(delay, backOff(1))
.put(showError({ error: `${mockError}. Failed!` }))
.run();
I'm trying to do something similar and am not sure how to simulate a delay in 4.0.0-beta2. In redux-saga 1.0.x, you can no longer use the yield call(delay, ms) approach and must call yield delay(ms) directly.
Any suggestions?
@cobarx We did it by relying on the fact that delay uses call under the hood:
return expectSaga(toastSaga)
.provide({
call: (effect, next) => {},
})
.take(foo)
.dispatch(bar)
.silentRun()
You can handle different calls by using effect or next (as in the docs).
Would love a more built-in way to do this though.
@michaelgmcd That worked for me, thanks.
I had my delay in a race and managed to test it by providing both race() and call().
const [undo] = yield race([notificationPromise, delay(500)]);
expectSaga(...)
.provide({
race: () => [false],
call: () => false,
})
This is how I did it. I have to check whether the name of the Function is 'delayP' or not. Otherwise, it mocks every calls in my *Saga() method.
Fake
const provideDelay = ({ fn }, next) => (fn.name === 'delayP') ? null : next();
Usage to Test addWorkoutSaga
it('should call addWorkoutSaga function', () => {
return expectSaga(addWorkoutSaga, fakeWorkoutPayload)
.provide([
{ call: provideDelay },
[matchers.call.fn(WorkoutService.add), null],
[matchers.call.fn(fetchWorkoutsSaga), null]
])
.call(WorkoutService.add, fakeWorkoutPayload.payload)
.put(addWorkout.success())
.call(fetchWorkoutsSaga)
.run();
});
addWorkoutSaga
export function* addWorkoutSaga({ payload }) {
try {
yield put(beginAjaxCall());
// throw error intentionally for calories lower than 50
if (payload.calories < 50)
payload.calories = '';
yield delay(1000);
yield call(WorkoutService.add, payload);
yield call(toast.success, "Item added successfully.");
yield put(closeModal(Modal.AddWorkout));
yield put(addWorkout.success());
yield call(fetchWorkoutsSaga);
}
catch (error) {
yield put(addWorkout.failure({ errorMessage: error.statusText }));
yield call(toast.error, "Error occured. Please try again.");
}
}
I was able to test it with a race but without using providers or anything weird by using a call on the delay. I was thrown in the wrong direction for a while by @cobarx's comment about not being able to use yield call(delay, ms), but that is because delay itself is a promise now not an effect.
So for me I used the delay inside a race which needs an effect, so I had the following code to test my delay
//Actual
yield race({
delay: call(delay, ms),
cancel: take(cancelDelayAction)
});
//Test
.race({
delay: call(delay, ms),
cancel: take(cancelDelayAction)
})
which made my test work as expected.
before i was trying to use
yield race({
delay: delay(ms),
cancel: take(cancelDelayAction)
});
which resulted in the test failing
Would be good to change the 'yield delay' values via matchers or so...
For now we had to mock it too... :(
Although not ideal, I got around the issue by using a proxy method for my delay:
export function* oneSecondDelay(): Saga<void> {
yield delay(1000);
}
which I can then mock:
.provide([
[matchers.call.fn(sagas.oneSecondDelay), null],
])
Digging this up to provide a new solution we found:
.provide([
[matchers.call.fn(delayP), null]
])
This takes advantage of redux-saga calling call on delayP under the hood here:
https://github.com/redux-saga/redux-saga/blob/d05370f12f49f29fea78767fdf5b7ba3621b60f7/packages/core/src/internal/io.js#L281
delayP
where are you importing delayP from? I'm getting an error if I try to get it form redux-saga. Can you please add more complete example?
Yeah for sure!
import delayP from '@redux-saga/delay-p'
Here's one of our test files:
https://github.com/AudiusProject/audius-client/blob/4114a2d8bfae8c31841acde32b1d1882b81806c1/packages/web/src/pages/audio-rewards-page/store/store.test.ts#L314
const provideDelay = ({ fn }, next) => (fn.name === 'delayP') ? null : next();
Thanks, @ttcg, It works well. Moreover, I can check if the delay function was called with a specific parameter
import { delay } from 'redux-saga/effects';
it('should call delay with 5000', async () => {
const { effects } = await expectSaga(testedSaga)
.provide([
{ call: provideDelay },
])
.run();
expect(effects.call[0]).toEqual(delay(5000))
});