SimplGy
SimplGy

Reputation: 20437

Testing Observables -- check for side effects on subscribe

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:

Upvotes: 1

Views: 919

Answers (2)

SimplGy
SimplGy

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.

  1. Create a fake cold observable that acts like your actual operation
  2. Use spyOn to return that value from your "do the real work" function
  3. Call the external api
  4. Verify the runCount on the fake cold observable
scheduler.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

Ian MacDonald
Ian MacDonald

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

Related Questions