Testing BehaviorSubject with RxJS Marble - rxjs

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.

Related

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);
});

withLatestFrom unexpected behavior

I have an unexpected behavior with the operator withLatestFrom.
Output
map a
a
map a <== why a is mapped again ?
map b
b
const { Subject, operators } = window.rxjs
const { map, withLatestFrom } = operators
const createA = new Subject()
const createB = new Subject()
const a = createA.pipe(
map(() => console.log('map a'))
)
const b = createB.pipe(
withLatestFrom(a),
map(() => console.log('map b'))
)
a.subscribe(() => { console.log('a') })
b.subscribe(() => { console.log('b') })
createA.next()
createB.next()
<script src="https://unpkg.com/#reactivex/rxjs#6.6.3/dist/global/rxjs.umd.js"></script>
I found that the operator share() allows multiple subscribers.
const a = createA.pipe(
map(() => console.log('map a')),
share()
)
The problem here isn't with withLatestFrom() but rather with how subscriptions work. Observables are lazy and don't run until you subscribe. Each new subscriptions will run the observable again.
const stream$ = from([1,2,3]);
stream$.subscribe(console.log) // output: 1 2 3
stream$.subscribe(console.log) // output: 1 2 3
In this case, `from([1,2,3)] ran twice. If I alter my stream, anything I do will happen for each subscriber.
const stream$ = from([1,2,3]).pipe(
tap(_ => console.log("hi"))
);
stream$.subscribe(console.log) // output: hi 1 hi 2 hi 3
stream$.subscribe(console.log) // output: hi 1 hi 2 hi 3
The final piece of the puzzle is this: internally withLatestFrom() subscribes to the stream that you give it. Just like an explicit .subscribe() runs the observable, so does withLatestFrom() once it's subscribed to.
You can use shareReplay to cache the latest values and replay them instead of running the observable again. It's one way to manage a multicasted stream:
const createA = new Subject()
const createB = new Subject()
const a = createA.pipe(
tap(() => console.log('tap a')),
shareReplay(1)
)
const b = createB.pipe(
withLatestFrom(a),
tap(() => console.log('tap b'))
)
a.subscribe(() => { console.log('a') })
b.subscribe(() => { console.log('b') })
createA.next()
createB.next()
Now a.subscribe() and withLatestFrom(a) are both getting a buffered value that only gets run when createA.next() is executed.
As an aside, mapping a value to nothing is bad habit to get into. Consider the following code:
from([1,2,3]).pipe(
map(val => console.log(val))
).subscribe(val => console.log(val));
This will output
1
undefined
2
undefined
3
undefined
because you're actually mapping each value to nothing. tap on the other hand doesn't change the source observable, so it's a much better tool for debugging and/or side effects that don't alter the stream
from([1,2,3]).pipe(
tap(val => console.log(val))
).subscribe(val => console.log(val));
This will output
1
1
2
2
3
3

Can a subscription remain subscribed if returning a still subscribed observable from a switchmap

Consider the following:
a$ = someObservable$.pipe(
switchMap(data => liveForEver$)
);
a$.subscribe();
a$.unsubscribe();
Now, liveForEver$ as the name suggests is subscribed to by other parts of the code. Could it be that a$ will stay subscribed after a$ is unsubscribed because switchMap returns a 'living' observable?
When an operator is defined, it usually has behavior to unsubscribe to child subscriptions when it is unsubscribed to. If you make a custom operator and fail to do this, then you'll likely create memory leaks. Consider the following custom operator:
function timesTwo(input$: Observable<number>): Observable<number> {
return new Observable<number>(observer => {
input$.subscribe({
next: val => observer.next(val * 2),
complete: () => observer.complete(),
error: err => observer.error()
});
return {
// I should $input.unsubscribe()
unsubscribe: () => {/*Do Nothing*/}
}
});
}
function timesTwoPipeable<T>(): MonoTypeOperatorFunction<T> {
return input$ => timesTwo(input$);
}
Here I've created my own custom rxjs operator that multiplies a stream of inputs by two. So 1:
const subscription = interval(1000).pipe(map(x => x * 2))
.subscribe(console.log);
setTimeout(() => subscription.unsubscribe(), 5000);
and 2:
const subscription = timesTwo(interval(1000))
.subscribe(console.log);
setTimeout(() => subscription.unsubscribe(), 5000);
and 3:
const subscription = interval(1000).pipe(timesTwoPipeable())
.subscribe(console.log);
setTimeout(() => subscription.unsubscribe(), 5000);
All have identical outputs to the console, but 2 and 3 both subscribe to the interval stream and then do not unsubscribe to it. So the second two quietly create a memory leak. You could test this yourself by changing interval(1000) to interval(1000).pipe(tap(_ => console.log("Still Alive"))) in all three examples.
All the built-in RxJS operators clean up after themselves. If you build your own, be sure to do the same!
Something I noticed in your question is that you tried to unsubscribe to an observable. I'm surprised that didn't create an error.
My inderstanding is that:
a$.subscribe();
a$.unsubscribe();
should be:
const sub = a$.subscribe();
sub.unsubscribe();

RxJs test for multiple values from the stream

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

Subscribe two times to one observable

first func:
updateMark(item: MarkDTO) {
this.service
.put(item, this.resource)
.subscribe(() => this.markEdit = null);
}
second func:
put(item: MarkDTO, rcc: string): Observable<MarkDTO> {
const rdto = new MarkRDTO(item);
const url = `${this.getUrl('base')}${rcc}/marks/${rdto.rid}`;
const obs = this.http.put<MarkDTO>(url, rdto, { withCredentials: true })
.pipe(map((r: MarkDTO) => new MarkDTO(r)))
.share();
obs.subscribe(newMark => this.storage.update(newMark, rcc));
return obs;
}
in service i need to update data after request
but also in same time i need to clear current editItem
all of it must be done after i subscribe to one httpRequest
.share() - suport from rxjs-compat package (i want to remove this dep in closest time)
without .share() - work only 1 of 2 steps
current rxjs version is 6.3.3
Help who can...
There is a pipeable share operator, that you would use the same way you use map() (i.e. inside pipe()) and thus doesn't need rxjs-compat.
But you don't need share() here. All you need is the tap() operator:
put(item: MarkDTO, rcc: string): Observable<MarkDTO> {
const rdto = new MarkRDTO(item);
const url = `${this.getUrl('base')}${rcc}/marks/${rdto.rid}`;
return this.http.put<MarkDTO>(url, rdto, { withCredentials: true })
.pipe(
map(r => new MarkDTO(r)),
tap(newMark => this.storage.update(newMark, rcc))
);
}

Resources