Reputation: 20437
I want to test the way an observable will behave from a consumer's perspective.
I can't tell if there are going to be side effects when I subscribe (cold) or not (hot).
Is there a way I can validate this behavior in a unit test?
I have TestScheduler
from rxjs/testing wired up, but I'm not seeing a good way to validate the number of times the observable has been created.
// ...the create method has been mocked to emit after 3 frames
const create$ = api.create(potato).pipe(
tap(console.log.bind(null, 'object created'))
);
create$.subscribe();
create$.subscribe();
create$.subscribe();
// Test how many times create$ has gotten a subscription, generated a cold observable, and completed.
const timing = '---(a|)'; // wait 3 frames, emit value of `a`, complete
const values = { a: potato };
expectObservable(create$).toBe(timing, values);
This test passes, but the "object created" message fires four times (3 for my subscribes, and one from middleware).
I want to write a failing test (true negative) before I change the observable's behavior to match what I'd like for api.create
.
How can I validate that the create behavior is only executed once?
I've tried:
spyOn
, but the actual create method is only called once.Array.isArray(create$.observers)
-- too indirect, only checks if it's hot, not if it behaves as expected.tap(() => runCount++)
\ expect(runCount).toBe(1)
-- works if I flush the scheduler, but seems outside of the norm for rxjs testing.Observable.create
with a factory function to manually track run counts. Also works, somewhat verbose.Upvotes: 1
Views: 919
Reputation: 20437
Here's the best way I've found so far to verify in a unit test that an inner observable is called only once for every subscription.
spyOn
to return that value from your "do the real work" functionscheduler.run(rx => {
let runCount = 0;
const timing = '---(a|)';
const values = { a: {x:42} };
// This represents the inner cold observable.
// We want to validate that it does/does not get called once per subscription
const mockedCreate$ = rx.cold(timing, values).pipe(
tap(() => runCount++),
);
spyOn(api, 'doCreate').and.returnValue(mockedCreate$);
const create$ = api.create({x:42}); // internally, calls doCreate
create$.subscribe();
create$.subscribe();
create$.subscribe();
// Explanation:
// If api.create wasn't multicasting/sharing the result of the doCreate
// operation, we'd see multiple actual save operations, not just 1
rx.expectObservable(create$).toBe(timing, values);
scheduler.flush();
expect(runCount).toBe(1, 'how often the "real" create operation ran');
});
Upvotes: 0
Reputation: 14030
I'm not sure I follow what you're asking, but I can address your concern about how many times your observable is created:
Once.
const create$ = api.create(potato)
This creates your observable. Your .pipe
attachment to the observable is part of the data path from the point of your observable to the subscribers.
potato ---(pipe)--->.subscribe()
+----(pipe)--->.subscribe()
+----(pipe)--->.subscribe()
+----(pipe)--->(expectObservable inspection)
Instead, it seems that you may want to put in an extra pipe there to share the result. Perhaps unsurprisingly, this pipe is called share
.
Input
import { Observable, Subject } from 'rxjs';
import { share, tap } from 'rxjs/operators';
let obj: Subject<string> = new Subject<string>();
let obs: Observable<string> = obj.pipe(tap(() => console.log('tap pipe')));
obs.subscribe((text) => console.log(`regular: ${text}`));
obs.subscribe((text) => console.log(`regular: ${text}`));
obs.subscribe((text) => console.log(`regular: ${text}`));
let shared: Observable<string> = obs.pipe(share());
shared.subscribe((text) => console.log(`shared: ${text}`));
shared.subscribe((text) => console.log(`shared: ${text}`));
shared.subscribe((text) => console.log(`shared: ${text}`));
obj.next('Hello, world!');
Output
tap pipe
regular: Hello, world!
tap pipe
regular: Hello, world!
tap pipe
regular: Hello, world!
tap pipe
shared: Hello, world!
shared: Hello, world!
shared: Hello, world!
Upvotes: 0