RxJs test for multiple values from the stream - rxjs

Given the following class:
import { BehaviorSubject } from 'rxjs';
import { map } from 'rxjs/operators';
export class ObjectStateContainer<T> {
private currentStateSubject = new BehaviorSubject<T>(undefined);
private $currentState = this.currentStateSubject.asObservable();
public $isDirty = this.$currentState.pipe(map(t => t !== this.t));
constructor(public t: T) {
this.update(t);
}
undoChanges() {
this.currentStateSubject.next(this.t);
}
update(t: T) {
this.currentStateSubject.next(t);
}
}
I would like to write some tests validating that $isDirty contains the value I would expect after performing various function invocations. Specifically I would like to test creating a variable, updating it and then undoing changes and validate the value of $isDirty for each stage. Currently, I've seen two way of testing observables and I can't figure out how to do this test with either of them. I would like the test to do the following:
Create a new ObjectStateContainer.
Assert that $isDirty is false.
Invoke update on the ObjectStateContainer.
Assert that $isDirty is true.
Invoke undoChanges on the ObjectStateContainer.
Assert that $isDirty is false.
import { ObjectStateContainer } from './object-state-container';
import { TestScheduler } from 'rxjs/testing';
class TestObject {
}
describe('ObjectStateContainer', () => {
let scheduler: TestScheduler;
beforeEach(() =>
scheduler = new TestScheduler((actual, expected) =>
{
expect(actual).toEqual(expected);
})
);
/*
SAME TEST AS ONE BELOW
This is a non-marble test example.
*/
it('should be constructed with isDirty as false', done => {
const objectStateContainer = new ObjectStateContainer(new TestObject());
objectStateContainer.update(new TestObject());
objectStateContainer.undoChanges();
/*
- If done isn't called then the test method will finish immediately without waiting for the asserts inside the subscribe.
- Using done though, it gets called after the first value in the stream and doesn't wait for the other two values to be emitted.
- Also, since the subscribe is being done here after update and undoChanges, the two previous values will already be gone from the stream. The BehaviorSubject backing the observable will retain the last value emitted to the stream which would be false here.
I can't figure out how to test the whole chain of emitted values.
*/
objectStateContainer
.$isDirty
.subscribe(isDirty => {
expect(isDirty).toBe(false);
expect(isDirty).toBe(true);
expect(isDirty).toBe(false);
done();
});
});
/*
SAME TEST AS ONE ABOVE
This is a 'marble' test example.
*/
it('should be constructed with isDirty as false', () => {
scheduler.run(({ expectObservable }) => {
const objectStateContainer = new ObjectStateContainer(new TestObject());
objectStateContainer.update(new TestObject());
objectStateContainer.undoChanges();
/*
- This will fail with some error message about expected length was 3 but got a length of one. This seemingly is happening because the only value emitted after the 'subscribe' being performed by the framework is the one that gets replayed from the BehaviorSubject which would be the one from undoChanges. The other two have already been discarded.
- Since the subscribe is being done here after update and undoChanges, the two previous values will already be gone from the stream. The BehaviorSubject backing the observable will retain the last value emitted to the stream which would be false here.
I can't figure out how to test the whole chain of emitted values.
*/
const expectedMarble = 'abc';
const expectedIsDirty = { a: false, b: true, c: false };
expectObservable(objectStateContainer.$isDirty).toBe(expectedMarble, expectedIsDirty);
});
});
});

I'd opt for marble tests:
scheduler.run(({ expectObservable, cold }) => {
const t1 = new TestObject();
const t2 = new TestObject();
const objectStateContainer = new ObjectStateContainer(t1);
const makeDirty$ = cold('----(b|)', { b: t2 }).pipe(tap(t => objectStateContainer.update(t)));
const undoChange$ = cold('----------(c|)', { c: t1 }).pipe(tap(() => objectStateContainer.undoChanges()));
const expected = ' a---b-----c';
const stateValues = { a: false, b: true, c: false };
const events$ = merge(makeDirty$, undoChange$);
const expectedEvents = ' ----b-----(c|)';
expectObservable(events$).toBe(expectedEvents, { b: t2, c: t1 });
expectObservable(objectStateContainer.isDirty$).toBe(expected, stateValues);
});
What expectObservable does is to subscribe to the given observable and turn each value/error/complete event into a notification, each notification being paired with the time frame at which it had arrived(Source code).
These notifications(value/error/complete) are the results of an action's task. An action is scheduled into a queue. The order in which they are queued is indicated by the virtual time.
For example, cold('----(b|)') means: at frame 4 send the value b and a complete notification.
If you'd like to read more about how these actions and how they are queued, you can check out this SO answer.
In our case, we're expecting: a---b-----c, which means:
frame 0: a(false)
frame 4: b(true)
frame 10: c(false)
Where are these frame numbers coming from?
everything starts at frame 0 and that at moment the class is barely initialized
cold('----(b|)) - will emit t2 at frame 4
cold('----------(c|)') - will call objectStateContainer.undoChanges() at frame 10

Related

Testing BehaviorSubject with RxJS Marble

I have a problem with testing BehaviorSubject using rxjs marble.
Minimal reproduction:
scheduler.run(({ expectObservable }) => {
const response$ = new BehaviorSubject(1);
expectObservable(response$).toBe('ab', { a: 1, b: 2 });
response$.next(2);
});
Error:
Expected $.length = 1 to equal 2.
Expected $[0].notification.value = 2 to equal 1.
In my case response$ is returned from some service's method.
How can I test it?
Can you try switching the order and see if that helps?
scheduler.run(({ expectObservable }) => {
const data$ = new ReplaySubject<number>();
const response$ = new BehaviorSubject(1);
response$.subscribe(num => data$.next(num));
response$.next(2);
expectObservable(data$).toBe('(ab)', { a: 1, b: 2 });
});
I think with the expectObservable, that's when the observable is subscribed and tested.
Edit
You need to use ReplaySubject instead of BehaviorSubject because BehaviorSujbect only returns the last emission of the Subject. Check out my edit above. I got inspired by this answer here: https://stackoverflow.com/a/62773431/7365461.

How to remove element from BehaviorSubject array?

There is an array in public users = new BehaviorSubject<User[]>([]).
I want to delete element from this observable and refresh it.
My solution is:
const idRemove = 2;
this.users.next(this.user.getValue().filter((u) => u.id !== idRemove)
But I seem I use wrong way of using RXJS
Toward Idiomatic RxJS
Using subscribe instead of .value.
interface User {
id: number
}
const users$ = new BehaviorSubject<User[]>([
{id:1},
{id:2},
{id:3}
]);
function removeId(idRemove: number) {
users$.pipe(
take(1),
map(us => us.filter(u => u.id !== idRemove))
).subscribe(
users$.next.bind(users$)
);
}
users$.subscribe(us =>
console.log("Current Users: ", us)
);
removeId(2);
removeId(1);
removeId(3);
Output:
Current Users: [ { id: 1 }, { id: 2 }, { id: 3 } ]
Current Users: [ { id: 1 }, { id: 3 } ]
Current Users: [ { id: 3 } ]
Current Users: []
To handle state within RxJS pipes you can use the Scan operator
Useful for encapsulating and managing state. Applies an accumulator (or "reducer function") to each value from the source after an initial state is established -- either via a seed value (second argument), or from the first value from the source.
const { Subject, merge } = rxjs;
const { scan, map } = rxjs.operators;
// This function is used to apply new users to the state of the scan
const usersFn = users => state => users
// This function is used to remove all matching users with the given id from the state of the scan
const removeFn = removeId => state => state.filter(user => user.id !== removeId)
// This Subject represents your old user BehaviorSubject
const users$$ = new Subject()
// This Subject represents the place where this.users.next(this.user.getValue().filter((u) => u.id !== idRemove) was called
const remove$$ = new Subject()
// This is your new user$ Observable that handles a state within its pipe. Use this Observable in all places where you need your user Array instead of the user BehaviorSubject
const user$ = merge(
// When users$$ emits the usersFn is called with the users argument (1. time)
users$$.pipe(map(usersFn)),
// When remove$$ emits the removeFn is called with the removeId argument (1. time)
remove$$.pipe(map(removeFn))
).pipe(
// Either the usersFn or removeFn is called the second time with the state argument (2. time)
scan((state, fn) => fn(state), [])
)
// Debug subscription
user$.subscribe(console.log)
// Test emits
users$$.next([
{id: 1, name: "first"},
{id: 2, name: "second"}
])
remove$$.next(2)
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/7.4.0/rxjs.umd.min.js"></script>
Ben Lesh (main Contributor of RxJS) wrote an anser about why not to use getValue in RxJS: The only way you should be getting values "out of" an Observable/Subject is with subscribe!
Using getValue() is discouraged in general for reasons explained here even though I'm sure there are exception where it's fine to use it. So a better way is subscribing to get the latest value and then changing it:
this.users
.pipe(take(1)) // take(1) will make sure we're not creating an infinite loop
.subscribe(users => {
this.users.next(users.filter((u) => u.id !== idRemove);
});

Nestjs #Sse : return result of a promise in rxjs observable

I am trying to go a simple step beyond the nest doc example in implementing #Sse() in a controller but I never used rxjs untill now so Im a bit confused.
The flow is :
client send a POST request with a file payload
server (hopefully) sends back the newly created project with a prop status:UPLOADED
client subscribe to sse route described below passing as param the projectId it just received from server
in the meantime server is doingSomeStuff that could take from 10sec to a min. When doingSomeStuff is done, project status is updated in db from UPLOADED to PARSED
My need is for the #Sse decorated function to execute at x interval of time a "status-check" and return project.status (that may or may not have been updated at the time)
My present code :
#Sse('sse/:projectId')
sse(#Param('projectId') projectId: string): Observable<any> {
const projId$ = from(this.projectService.find(projectId)).pipe(
map((p) => ({
data: {
status: p.status,
},
})),
);
return interval(1000).pipe(switchMap(() => projId$));
}
I don't put code of the service here as it a simple mongooseModel.findById wrapper.
My problem is the status returned remains UPLOADED and is never updated. It doesnt seem the promise is reexecuted at every tick. If I console.log inside my service I can see my log being printed only once with the initial project value while I expect to see a new log at each tick.
This is a two-step process.
We create an observable out of the promise generated by this.service.findById() using the from operator in rxjs. We also use the map operator to set the format of the object we need when someone subscribes to this observable.
We want to return this observable every x seconds. interval(x) creates an observable that emits a value after every x milliseconds. Hence, we use this and then switchMap to the projId$ whenever the interval emits a value. The switchMap operator switches to the inner observable whenever the outer observable emits a value.
Please note: Since your server may take 10 sec, to min for doing the operation, you should set the intervalValue accordingly. In the code snippet below, I've set it to 10,000 milli seconds which is 10 seconds.
const intervalValue = 10000;
#Sse('sse/:projectId')
sse(#Param('projectId') projectId: string): Observable < any > {
return interval(intervalValue).pipe(
switchMap(() => this.projectService.find(projectId)),
map((p) => ({
data: {
status: p.status,
}
})));
}
// OR
#Sse('sse/:projectId')
sse(#Param('projectId') projectId: string): Observable < any > {
const projId$ = defer(() => this.service.findById(projectId)).pipe(
map(() => ({
data: {
_: projectId
}
}))
);
return interval(intervalValue).pipe(switchMap(() => projId$));
}
#softmarshmallow
You can watch model changes and use observable stream to send it.
Something like this
import { Controller, Param, Sse } from '#nestjs/common'
import { filter, map, Observable, Subject } from 'rxjs'
#Controller('project')
export class ProjectStatusController {
private project$ = new Subject()
// watch model event
// this method should be called when project is changed
onProjectChange(project) {
this.project$.next(project)
}
#Sse('sse/:projectId')
sse(#Param('projectId') projectId: string): Observable<any> {
return this.project$.pipe(
filter((project) => project.projectId === projectId),
map((project) => ({
data: {
status: project.status,
},
})),
)
}
}

React component not receiving intermediate state when chaining actions in redux-saga

I have two actions TEST and TEST_DONE which both increment an id property in my redux state. I am using redux-saga to dispatch the second action TEST_DONE automatically whenever I dispatch the first action TEST from my component.
I expect the order of execution to go like this:
component renders with initial value of testState.id = 0
component dispatches TEST action
component re-renders with testState.id = 1
saga dispatches the TEST_DONE action
component re-renders with testState.id = 2
Instead my component only re-renders when testState.id is updated to 2. I can't see the 1 value in the getSnapshotBeforeUpdate function. It shows 0 as the previous prop.
Why does the prop jump from 0 to 2 without receiving 1 in between?
saga.js:
export function* TestSagaFunc() {
yield put({
type: actions.TEST_DONE
});
};
export default function* rootSaga() {
yield all([
yield takeEvery(actions.TEST, TestSagaFunc),
]);
};
action.js:
const actions = {
TEST: 'TEST',
TEST_DONE: 'TEST_DONE',
callTest: (id) => ({
type: actions.TEST,
payload: {
id
}
}),
};
export default actions;
reducer.js:
const initState = {
testState: {
id: 0
}
};
export default function TestReducers ( state=initState, { type, ...action}) {
switch(type) {
default:
return state;
case actions.TEST: {
const { id } = state.testState;
const nextId = id + 1;
return {
...state,
testState: {
...state.testState,
id: nextId
}
};
};
case actions.TEST_DONE: {
const { id } = state.testState;
const nextId = id + 1;
return {
...state,
testState: {
...state.testState,
id: nextId
}
};
}
};
};
console output from component getSnapshotBeforeUpdate
Summarizing my comments from the question:
The redux state is indeed being updated as you've seen, but a component is not guaranteed to render every intermediate state change based on the way react batches state changes. To test this you can try importing delay from redux-saga/effects and adding yield delay(1000); before calling yield put in TestSagaFunc so the two state updates don't get batched together.
This is just a trick to illustrate the effects of batching and almost certainly not what you want to do. If you need the intermediate state to be rendered you could dispatch TEST_DONE from the component being rendered with a useEffect (or componentDidUpdate) to ensure that the component went through one render cycle with the intermediate state. But there is no way to force your component to render intermediate reducer states that are batched together.

Verify observable value from BehaviorSubject in subscription

I've got a simple service that queries the web and then based on the return, it emits an event through a BehaviorSubject
export class ProducerService {
static readonly baseUrl = `${environment.apiUri}/producers`
private readonly admin = new BehaviorSubject<boolean>(false)
readonly admin$ = this.admin.asObservable()
constructor(private readonly http: HttpClient) {
}
queryAdmin(): void {
this.http.get<boolean>(`${ProducerService.baseUrl}/admin`)
.subscribe(x => this.admin.next(x))
}
}
Now I'm trying to write a test that verifies if true is passed in that the admin$ variable gets set to true. I tried it like this
it('should emit true when an admin', async(() => {
service.admin$.subscribe(x => expect(x).toBeTrue())
service.queryAdmin()
const req = httpMock.expectOne({url: `${ProducerService.baseUrl}/admin`, method: 'GET'})
req.flush(true)
}))
That fails though saying "Expected false to be true". What am I doing wrong?
The BehaviorSubject is "hot" so it is ready to go when you subscribe to it and it has an initial value of false, then you're asserting false toBeTrue.
Try to filter out the false values using the filter operator of Rxjs.
import { filter } from 'rxjs/operators';
....
it('should emit true when an admin', async((done) => {
service.admin$.pipe(
filter(admin => !!admin), // the value of admin has to be true for it to go into the subscribe block
).subscribe(x => {
expect(x).toBeTrue();
done(); // add the done function as an argument and call it to ensure
}); // test execution made it into this subscribe and thus the assertion was made
// Calling done, tells Jasmine we are done with our test.
service.queryAdmin()
const req = httpMock.expectOne({url: `${ProducerService.baseUrl}/admin`, method: 'GET'})
req.flush(true)
}))
Had to do multiple things here. Couldn't use async or it didn't like the done method. Had to do the filter like #AliF50 suggested, and I had to pass in a value of 1 instead of true. So I ended up with this for the test:
it('should emit true when an admin', (done) => {
service.admin$
.pipe(filter(x => x))
.subscribe(x => {
expect(x).toBeTrue()
done()
})
service.queryAdmin()
const req = httpMock.expectOne({url: `${ProducerService.baseUrl}/admin`, method: 'GET'})
req.flush(1)
})
That also meant I had to modify my queryAdmin method so that I did the !! like so:
queryAdmin(): void {
// This is being done for the producer.service.spec.ts file because it
// won't decode a 'true' value automatically, so I have to pass in a 1
// as the body (i.e. a true value) and then this !!x converts that to true.
// noinspection PointlessBooleanExpressionJS
this.http.get<boolean>(`${ProducerService.baseUrl}/admin`).subscribe(x => this.admin.next(!!x))
}

Resources