Testing NGRX effect with delay - rxjs

I want to test an effect that works as follows:
Effect starts if LoadEntriesSucces action was dispatched
It waits for 5 seconds
After 5 seconds passes http request is send
When response arrives, new action is dispatched (depending, whether response was succes or error).
Effect's code looks like this:
#Effect()
continuePollingEntries$ = this.actions$.pipe(
ofType(SubnetBrowserApiActions.SubnetBrowserApiActionTypes.LoadEntriesSucces),
delay(5000),
switchMap(() => {
return this.subnetBrowserService.getSubnetEntries().pipe(
map((entries) => {
return new SubnetBrowserApiActions.LoadEntriesSucces({ entries });
}),
catchError((error) => {
return of(new SubnetBrowserApiActions.LoadEntriesFailure({ error }));
}),
);
}),
);
What I want to test is whether an effect is dispatched after 5 seconds:
it('should dispatch action after 5 seconds', () => {
const entries: SubnetEntry[] = [{
type: 'type',
userText: 'userText',
ipAddress: '0.0.0.0'
}];
const action = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
const completion = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
actions$ = hot('-a', { a: action });
const response = cold('-a', {a: entries});
const expected = cold('- 5s b ', { b: completion });
subnetBrowserService.getSubnetEntries = () => (response);
expect(effects.continuePollingEntries$).toBeObservable(expected);
});
However this test does not work for me. Output from test looks like this:
Expected $.length = 0 to equal 3.
Expected $[0] = undefined to equal Object({ frame: 20, notification: Notification({ kind: 'N', value: undefined, error: undefined, hasValue: true }) }).
Expected $[1] = undefined to equal Object({ frame: 30, notification: Notification({ kind: 'N', value: undefined, error: undefined, hasValue: true }) }).
Expected $[2] = undefined to equal Object({ frame: 50, notification: Notification({ kind: 'N', value: LoadEntriesSucces({ payload: Object({ entries: [ Object({ type: 'type', userText: 'userText', ipAddress: '0.0.0.0' }) ] }), type: '[Subnet Browser API] Load Entries Succes' }), error: undefined, hasValue: true }) }).
What should I do to make this test work?

Like mentioned in another answer, one way to test that effect would be by using the TestScheduler but it can be done in a simpler way.
We can test our asynchronous RxJS code synchronously and deterministically by virtualizing time using the TestScheduler. ASCII marble diagrams provide a visual way for us to represent the behavior of an Observable. We can use them to assert that a particular Observable behaves as expected, as well as to create hot and cold Observables we can use as mocks.
For example, let's unit test the following effect:
effectWithDelay$ = createEffect(() => {
return this.actions$.pipe(
ofType(fromFooActions.doSomething),
delay(5000),
switchMap(({ payload }) => {
const { someData } = payload;
return this.fooService.someMethod(someData).pipe(
map(() => {
return fromFooActions.doSomethingSuccess();
}),
catchError(() => {
return of(fromFooActions.doSomethinfError());
}),
);
}),
);
});
The effect just waits 5 seconds after an initial action, and calls a service which would then dispatch a success or error action. The code to unit test that effect would be the following:
import { TestBed } from "#angular/core/testing";
import { provideMockActions } from "#ngrx/effects/testing";
import { Observable } from "rxjs";
import { TestScheduler } from "rxjs/testing";
import { FooEffects } from "./foo.effects";
import { FooService } from "../services/foo.service";
import * as fromFooActions from "../actions/foo.actions";
// ...
describe("FooEffects", () => {
let actions$: Observable<unknown>;
let testScheduler: TestScheduler; // <-- instance of the test scheduler
let effects: FooEffects;
let fooServiceMock: jasmine.SpyObj<FooService>;
beforeEach(() => {
// Initialize the TestScheduler instance passing a function to
// compare if two objects are equal
testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
TestBed.configureTestingModule({
imports: [],
providers: [
FooEffects,
provideMockActions(() => actions$),
// Mock the service so that we can test if it was called
// and if the right data was sent
{
provide: FooService,
useValue: jasmine.createSpyObj("FooService", {
someMethod: jasmine.createSpy(),
}),
},
],
});
effects = TestBed.inject(FooEffects);
fooServiceMock = TestBed.inject(FooService);
});
describe("effectWithDelay$", () => {
it("should dispatch doSomethingSuccess after 5 seconds if success", () => {
const someDataMock = { someData: Math.random() * 100 };
const initialAction = fromFooActions.doSomething(someDataMock);
const expectedAction = fromFooActions.doSomethingSuccess();
testScheduler.run((helpers) => {
// When the code inside this callback is being executed, any operator
// that uses timers/AsyncScheduler (like delay, debounceTime, etc) will
// **automatically** use the TestScheduler instead, so that we have
// "virtual time". You do not need to pass the TestScheduler to them,
// like in the past.
// https://rxjs-dev.firebaseapp.com/guide/testing/marble-testing
const { hot, cold, expectObservable } = helpers;
// Actions // -a-
// Service // -b|
// Results // 5s --c
// Actions
actions$ = hot("-a-", { a: initialAction });
// Service
fooServiceMock.someMethod.and.returnValue(cold("-b|", { b: null }));
// Results
expectObservable(effects.effectWithDelay$).toBe("5s --c", {
c: expectedAction,
});
});
// This needs to be outside of the run() callback
// since it's executed synchronously :O
expect(fooServiceMock.someMethod).toHaveBeenCalled();
expect(fooServiceMock.someMethod).toHaveBeenCalledTimes(1);
expect(fooServiceMock.someMethod).toHaveBeenCalledWith(someDataMock.someData);
});
});
});
Please notice that in the code I'm using expectObservable to test the effect using the "virtual time" from the TestScheduler instance.

you could use the done callback from jasmine
it('should dispatch action after 5 seconds', (done) => {
const resMock = 'resMock';
const entries: SubnetEntry[] = [{
type: 'type',
userText: 'userText',
ipAddress: '0.0.0.0'
}];
const action = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
const completion = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
actions$ = hot('-a', { a: action });
const response = cold('-a', {a: entries});
const expected = cold('- 5s b ', { b: completion });
subnetBrowserService.getSubnetEntries = () => (response);
effects.continuePollingEntries$.subscribe((res)=>{
expect(res).toEqual(resMock);
done()
})
});

The second notation doesn't work with jasmine-marbles, use dashes instead:
const expected = cold('------b ', { b: completion });

You will need to do 3 things
1- Inside your beforeEach, you need to override the internal scheduler of RxJs as follows:
import { async } from 'rxjs/internal/scheduler/async';
import { cold, hot, getTestScheduler } from 'jasmine-marbles';
beforeEach(() => {.....
const testScheduler = getTestScheduler();
async.schedule = (work, delay, state) => testScheduler.schedule(work, delay, state);
})
2- Replace delay, with delayWhen as follows:
delayWhen(_x => (true ? interval(50) : of(undefined)))
3- Use frames, I am not really sure how to use seconds for this, so I used frames. Each frame is 10ms. So for example my delay above is 50ms and my frame is -b, so that is the expected 10 ms + I needed another 50ms so this equals extra 5 frames which was ------b so as follows:
const expected = cold('------b ', { b: outcome });

Related

Testing NGRX effect that emits action with delay OR does not emit anything

I have an NGRX effect that - depending on the state - emits an action with a delay or it emits nothing.
I want to write a test, covering both situations.
This is the effect:
myEffect$ = createEffect(() =>
this.actions$.pipe(
ofType(MyAction),
filter(state => state.foo === false),
delay(4000),
map(state => myOtherAction())
)
);
The test for the situation where it should emit the otherAction with the delay works fine:
describe('emit my action', () => {
const action = MyAction();
it('should return a stream with myOtherAction', () => {
const scheduler = getTestScheduler();
scheduler.run(helpers => {
// build the observable with the action
actions = hot('-a', { a: action });
// define what is the expected outcome of the effect
const expected = {
b: MyOtherAction()
};
helpers.expectObservable(effects.myEffect$).toBe('- 4000ms b', expected);
});
});
});
But I have no clue how to test the other state, where it should NOT emit another action (the stream has zero length):
it('should return an empty stream', () => {
store.setState({
myFeature: {
foo: true
}
});
// ???
});
Please help :)
This will be tough to do because the filter will prevent the effect to ever return an observable.
Option 1:
// wherever you're dispatching MyAction, only dispatch it if the foo property is true on the created action
Option 2:
// Change the structure of the effect to return empty
import { EMPTY } from 'rxjs';
....
myEffect$ = createEffect(() =>
this.actions$.pipe(
ofType(MyAction),
delay(4000),
map(state => state.foo ? myOtherAction() : EMPTY)
)
);
The test:
import { EMPTY } from 'rxjs';
....
describe('emit my action', () => {
const action = MyAction();
action.foo = false; // set foo property to false
it('should return a stream with myOtherAction', () => {
const scheduler = getTestScheduler();
scheduler.run(helpers => {
// build the observable with the action
actions = hot('-a', { a: action });
// define what is the expected outcome of the effect
const expected = {
b: EMPTY // assert now it is empty
};
helpers.expectObservable(effects.myEffect$).toBe('- 4000ms b', expected);
});
});
})
Due to the hint from AliF50, I replaced the filter in the chain (which stops the Observable from emitting), by a "Noop Action" (= a normal action without any listeners on it). So instead of checking the foo property in the filter, I return the noopAction in the map, when foo is true, and the otherAction when it's false.
The effect:
myEffect$ = createEffect(() =>
this.actions$.pipe(
ofType(MyAction),
//filter(state => state.foo === false),
delay(4000),
map(state => state.foo !== false ? noopAction() : myOtherAction())
)
);
The test:
describe('emit my action', () => {
const action = MyAction();
it('should return a stream with myOtherAction', () => {
const scheduler = getTestScheduler();
scheduler.run(helpers => {
// build the observable with the action
actions = hot('-a', { a: action });
// define what is the expected outcome of the effect
const expected = {
b: MyOtherAction()
};
helpers.expectObservable(effects.myEffect$).toBe('- 4000ms b', expected);
});
});
it('should return a stream with noop action as foo is true', () => {
store.setState({
myFeature: {
foo: true
}
});
const scheduler = getTestScheduler();
scheduler.run(helpers => {
// build the observable with the action
actions = hot('-a', { a: action });
// define what is the expected outcome of the effect
const expected = {
b: NoopAction()
};
helpers.expectObservable(effects.myEffect$).toBe('- 4000ms b', expected);
});
});
});

rxjs subscription being called more often than expected

I have a BehaviorSubject stream of functions. I have an initialState object represented as an immutable Record. Those functions are scanned and used to manipulate the state. The code looks like this:
const initialState = Record({
todo: Record({
title: "",
}),
todos: List([Record({title: "first todo"})()])
})
const actionCreator = (update) => ({
addTodo(title) {
update.next((state) => {
console.log({title}); // for debugging reasons
const todo = Record({title})()
return state.set("todos", state.get("todos").push(todo))
})
},
typeNewTodoTitle(title) {
update.next((state) => state.set("todo", state.get("todo").set("title", title))
})
})
const update$ = new BehaviorSubject(state => state);
const actions = actionCreator(update$);
const state = update$.pipe(
scan(
(state, updater) => updater(state), initialState()
),
// share() without share weird things happen
)
I have a very simple test written for this
it("should only respond to and call actions once", () => {
const subscripition = chai.spy();
const addTodo = chai.spy.on(actions, 'addTodo');
const typeNewTodoTitle = chai.spy.on(actions, 'typeNewTodoTitle');
state
.pipe(
map(s => s.get("todo")),
distinctUntilChanged()
)
.subscribe(subscripition);
state
.pipe(
map(s => s.get("todos")),
distinctUntilChanged()
)
.subscribe(subscripition);
actions.addTodo('test');
expect(subscripition).to.have.been.called.twice // error
actions.typeNewTodoTitle('test');
expect(subscripition).to.have.been.called.exactly(3) // error
expect(addTodo).to.have.been.called.once
expect(typeNewTodoTitle).to.have.been.called.once
});
});
The first strange behavior is that subscription has been called 3 times and then 4 instead of 2 and then 3 times. The second strange behavior is that even though each action has only been called once, the console.log has been called twice. I can fix this problem by adding share() to the pipeline, but I can't figure out why that's required.

How to use useReducer and rxjs with react hooks?

I would like to use useReducer from react-hooks and rxjs together.
For example, I would like to fetch data from an API.
This is the code I wrote in order to do that:
RXJS hook:
function useRx(createSink, data, defaultValue = null) {
const [source, sinkSubscription] = useMemo(() => {
const source = new Subject()
const sink = createSink(source.pipe(distinctUntilChanged()));
const sinkSubscription = sink.subscribe()
return [source, sinkSubscription]
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
source.next(data)
}, [source, data])
useEffect(() => {
return () => {
sinkSubscription.unsubscribe()
};
}, [sinkSubscription])
}
Reducer code:
const dataFetchReducer = (state, action) => {
switch (action.type) {
case 'FETCH_LOADING':
return {
...state,
loading: true
};
case 'FETCH_SUCCESS':
return {
...state,
loading: false,
total: action.payload.total,
data: action.payload.data
};
case 'FETCH_FAILURE':
return {
...state,
error: action.payload
};
case 'PAGE':
return {
...state,
page: action.page,
rowsPerPage: action.rowsPerPage
};
default:
throw new Error();
}
};
How i mix them:
function usePaginationReducerEndpoint(callbackService) {
const defaultPagination = {
statuses: null,
page: 0,
rowsPerPage: 10,
data: [],
total: 0,
error: null,
loading: false
}
const [pagination, dispatch] = useReducer(dataFetchReducer, defaultPagination)
const memoPagination = useMemo(
() => ({
statuses: pagination.statuses,
page: pagination.page,
rowsPerPage: pagination.rowsPerPage
}),
[pagination.statuses, pagination.page, pagination.rowsPerPage]
);
useRx(
memoPagination$ =>
memoPagination$.pipe(
map(memoPagination => {
dispatch({type: "FETCH_LOADING"})
return memoPagination
}),
switchMap(memoPagination => callbackService(memoPagination.statuses, memoPagination.page, memoPagination.rowsPerPage).pipe(
map(dataPagination => {
dispatch({ type: "FETCH_SUCCESS", payload: dataPagination })
return dataPagination
}),
catchError(error => {
dispatch({ type: "FETCH_SUCCESS", payload: "error" })
return of(error)
})
))
),
memoPagination,
defaultPagination,
2000
);
function handleRowsPerPageChange(event) {
const newTotalPages = Math.trunc(pagination.total / event.target.value)
const newPage = Math.min(pagination.page, newTotalPages)
dispatch({
type: "PAGE",
page: newPage,
rowsPerPage: event.target.value
});
}
function handlePageChange(event, page) {
dispatch({
type: "PAGE",
page: page,
rowsPerPage: pagination.rowsPerPage
});
}
return [pagination, handlePageChange, handleRowsPerPageChange]
}
The code works but I'm wondering if this is luck or not...
Is it ok if I dispatch inside RXJS pipe ? What is the risk ?
If no, how can I mix both useReducer and RXJS ?
If it's not the good approach, what is the good one ?
I know this ressource: https://www.robinwieruch.de/react-hooks-fetch-data/. But I would like to mix the power of hooks and RXJS in order to use, for example, the debounce function with rxjs in an async request...
Thanks for your help,
All you need is a middleware to connect useReducer and rxjs rather than create one yourself.
Using useReducer will create a lot of potential hard to debug code and also need an independent container component to put useReducer to prevent accident global rerendering.
So I suggest that using redux places useReducer to create global state from a component and use redux-observable (RxJS 6-based middleware for Redux) as middleware to connect rxjs and redux.
if you know rxjs well, it will be very easy to use, as official web show, fetch data from api will be:
https://redux-observable.js.org/docs/basics/Epics.html
// epic
const fetchUserEpic = action$ => action$.pipe(
ofType(FETCH_USER),
mergeMap(action =>
ajax.getJSON(`https://api.github.com/users/${action.payload}`).pipe(
map(response => fetchUserFulfilled(response))
)
)
);

Testing if method with Promise was called (Jest)

I have an initializer method calling another method that returns a promise, like:
initStuffAfterLoad() {
const _this = this;
const theInterval = window.setInterval(function() {
if (thing) {
window.clearInterval(theInterval);
_this.getBanana()
.then(response => {
_this.getApple(response, _this);
});
}
}, 100);
}
and am needing to test whether getBanana was called (jest/sinon). So far I have:
test('init function calls getBanana', () => {
let thing = true
const getBananaSpy = sinon.spy();
sinon.stub(TheClass.prototype, 'getBanana').callsFake(getBananaSpy).resolves();
jest.useFakeTimers();
TheClass.prototype.initStuffAfterLoad();
jest.runOnlylPendingTimers();
expect(getBananaSpy.called).toBeTruthy();
TheClass.prototype.getBanana.restore();
});
However it still receives false at the assertion. I figure I'm not handling the Promise part correctly - what is the best practice way to do this?
I am not familiar with sinon, but here is a way to achieve your need with pure jest (even better it also checks that getApple is called when getBanana reseolves :))
jest.useFakeTimers()
const _this = {
getBanana: () => {},
getApple: () => {}
}
const initStuffAfterLoad = () => {
const theInterval = window.setInterval(function() {
window.clearInterval(theInterval);
_this.getBanana().then(response => {
_this.getApple(response, _this)
});
}, 100);
}
test('', () => {
let result
_this.getBanana = jest.fn(() => {
result = new Promise( resolve => { resolve() } )
return result
})
_this.getApple = jest.fn()
initStuffAfterLoad()
jest.runAllTimers()
expect(_this.getBanana.mock.calls.length).toBe(1)
return result.then(() => {
expect(_this.getApple.mock.calls.length).toBe(1)
})
})
code tested :)
PASS test\temp.test.js √ (25ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 2.489s

How to test observable containing a debounce operator?

How does one write a Jasmine test to test an observable with the debounce operator? I've followed this blog post and understand the principles of how it should be tested, but it just doesn't seem to work.
Below is the factory that I am using to create the observable:
import Rx from "rx/dist/rx.all";
import DOMFactory from "../utils/dom-factory";
import usernameService from "./username.service";
function createUsernameComponent(config) {
const element = DOMFactory(config);
const username = Rx.Observable
.fromEvent(element.find('input'), 'input')
.pluck('target', 'value')
.startWith(config.value);
const isAvailable = username
.debounce(500)
.tap(() => console.info('I am never called!'))
.flatMapLatest(usernameService.isAvailable)
.startWith(false);
const usernameStream = Rx.Observable.combineLatest(username, isAvailable)
.map((results) => {
const [username, isAvailable] = results;
return isAvailable ? username : ''
})
.distinctUntilChanged();
return Object.freeze({
stream: usernameStream,
view: element
});
}
export default createUsernameComponent;
Note that tap operator is never called by the test. However, it will be executed properly if I run this code on the browser.
Below is my attempt at the test:
import Rx from "rx/dist/rx.all";
import Username from "./username.component";
import DataItemBuilder from "../../../test/js/utils/c+j-builders";
import usernameService from "./username.service"
describe('Username Component', () => {
let input, username;
beforeEach(() => {
const usernameConfig = DataItemBuilder.withName('foo')
.withPrompt('label').withType('text').build();
const usernameComponent = Username(usernameConfig);
usernameComponent.stream.subscribe(value => username = value);
input = usernameComponent.view.find('input');
});
it('should set to a valid username after debounce', () => {
const scheduler = injectTestSchedulerIntoDebounce();
scheduler.scheduleRelative(null, 1000, () => {
doKeyUpTest('abcddd', 'abcdd');
scheduler.stop();
});
scheduler.start();
scheduler.advanceTo(1000);
});
function injectTestSchedulerIntoDebounce() {
const originalOperator = Rx.Observable.prototype.debounce;
const scheduler = new Rx.TestScheduler();
spyOn(Rx.Observable.prototype, 'debounce').and.callFake((dueTime) => {
console.info('The mocked debounce is never called!');
if (typeof dueTime === 'number') {
return originalOperator.call(this, dueTime, scheduler);
}
return originalOperator.call(this, dueTime);
});
return scheduler;
}
function doKeyUpTest(inputValue, expectation) {
input.val(inputValue);
input.trigger('input');
expect(username).toBe(expectation);
}
});
When I run the test, the fake debounce never gets called. I plan to mock the username service once I can get past the debounce.
In your test code you are triggering the input event inside the scheduleRelative function. This doesn't work because you are advancing 1000ms before doing the change. The debouncer then waits 500ms to debounce the isAvailable call but you already stopped the scheduler so time is not advancing afterwards.
What you should do is: trigger the input event before advancing the scheduler time or even better in a scheduleRelative function for a time <= 500ms in a and then inside the scheduleRelative function for 1000ms you have to call the expect function with the expected output and then stop the scheduler.
It should look like this:
it('should set to a valid username after debounce', () => {
const scheduler = injectTestSchedulerIntoDebounce();
scheduler.scheduleRelative(null, 500, () => {
input.val(inputValue);
input.trigger('input');
});
scheduler.scheduleRelative(null, 1000, () => {
expect(username).toBe(expectation);
scheduler.stop();
});
scheduler.start();
scheduler.advanceTo(1000);
});
In addition to that I have better experience with scheduleAbsolute instead of scheduleRelative because it is less confusing.
As per Simon Jentsch's answer, below is the answer using scheduleAbsolute instead of scheduleRelative:
import Rx from "rx/dist/rx.all";
import Username from "./username.component";
import DataItemBuilder from "../../../test/js/utils/c+j-builders";
import usernameService from "./username.service"
describe('Username Component', () => {
let input, username, promiseHelper;
const scheduler = new Rx.TestScheduler(0);
beforeEach(() => {
spyOn(usernameService, 'isAvailable').and.callFake(() => {
return Rx.Observable.just(true);
});
});
beforeEach(() => {
const usernameConfig = DataItemBuilder.withName('foo')
.withPrompt('label').withType('text').build();
const usernameComponent = Username(usernameConfig, scheduler);
usernameComponent.stream.subscribe(value => username = value);
input = usernameComponent.view.find('input');
});
it('should set the username for valid input after debounce', (done) => {
doKeyUpTest('abcddd', '');
scheduler.scheduleAbsolute(null, 100, () => {
expect(usernameService.isAvailable).not.toHaveBeenCalled();
expect(username).toBe('');
});
scheduler.scheduleAbsolute(null, 1000, () => {
expect(usernameService.isAvailable).toHaveBeenCalled();
expect(username).toBe('abcddd');
scheduler.stop();
done();
});
scheduler.start();
});
function doKeyUpTest(inputValue, expectation) {
input.val(inputValue);
input.trigger('input');
expect(username).toBe(expectation);
}
});

Resources