logo logo

Mocking Observables in JavaScript Tests

main post image

There are different ways to test components that involve Observables. You might come across tests that are not written properly. These tests are sometimes only green accidentally or toggle between green and red. Let’s see what’s usually wrong in those cases and also look at a variant that works reliably. We present an example and are going through three different ways of testing Observables in JavaScript tests.

Testing Observables in JavaScript

For demonstration purposes, we are using an Angular application with Jasmine tests.
Let’s assume we want to fetch delivery methods for an online shop. And if the delivery method “Post” is available, the address of the customer should be fetched as well. To implement that functionality we created a ShipmentService with two dependencies: DeliveryMethodService and CustomerService.

export class ShipmentService {

  constructor(private deliveryMethodService: DeliveryMethodService,
              private customerService: CustomerService) {
  }

  public getShipmentOptions(customerId: number): Observable<ShipmentOption[]> {
    return this.deliveryMethodService.getDeliveryMethods().pipe(
      map((methods: DeliveryMethod[]) =>
        methods.map((method: DeliveryMethod) => ({deliveryMethod: method}))
      ),
      switchMap((shipmentOptions: ShipmentOption[]) => {
          if (shipmentOptions.find(option => option.deliveryMethod === 'Post')) {
            return this.customerService.getCustomerAddresses(customerId).pipe(
              map((addresses: Address[]) => {
                if (!addresses || addresses.length === 0) {
                  return shipmentOptions;
                } else {
                  return shipmentOptions
                    .filter(option => option.deliveryMethod !== 'Post')
                    .concat({ deliveryMethod: 'Post', address: addresses[0] });
                }
              })
            );
          } else {
            return of(shipmentOptions);
          }
        }
      )
    );
  }
}

1 – Returning an Observable with of(…)

The setup is as follows for our service tests:

beforeEach(() => {
  deliveryMethodService = {
    getDeliveryMethods: (): Observable<DeliveryMethod[]> => of(['Pickup', 'Post'])
  } as unknown as DeliveryMethodService;

  customerService = {
    getCustomerAddresses: (): Observable<Address[]> => of([address])
  } as unknown as CustomerService;

  shipmentService = new ShipmentService(deliveryMethodService, customerService);
});

I know that there are more elegant ways to mock dependencies. Here we just want the types and operations that are required.

In the test itself we subscribe to the Observable and do the assertion when we get the value:

it('should return the delivery methods with address if Post is available', () => {
  const shipmentOptions$: Observable<ShipmentOption[]> = shipmentService.getShipmentOptions(customerId);
  const expected: ShipmentOption[] = [
    { deliveryMethod: 'Pickup' },
    { deliveryMethod: 'Post', address },
  ];

  shipmentOptions$.subscribe((options: ShipmentOption[]) => {
    expect(options).toEqual(expected);
  });
});

For another test, we want another response and assign an Observable for that to the service method. In this case an empty array []:

it('should return the delivery methods without address if no address returned', () => {
  customerService.getCustomerAddresses = () => of([]);

  const shipmentOptions$: Observable<ShipmentOption[]> = shipmentService.getShipmentOptions(customerId);
  const expected: ShipmentOption[] = [
    { deliveryMethod: 'Pickup' },
    { deliveryMethod: 'Post' },
  ];

  shipmentOptions$.subscribe((options: ShipmentOption[]) => {
    expect(options).toEqual(expected);
  });
});

Everything looks fine:

Now there was a change in implementation. The following was added to the top of the getShipmentOptions method:

if (customerId === 2) {
  return EMPTY;
}

The customer with ID 2 will not get any delivery methods. Or let’s just assume we have made another mistake that emits nothing (EMPTY). Let’s run our 2 tests again. One should fail now because we are passing customer ID 2 to the method. So empty comes back and that is not what we expect:

it('should return the delivery methods with address if Post is available', () => {
  const shipmentOptions$: Observable<ShipmentOption[]> = shipmentService.getShipmentOptions(2);
  const expected: ShipmentOption[] = [
    {deliveryMethod: 'Pickup'},
    {deliveryMethod: 'Post', address},
  ];

  shipmentOptions$.subscribe((options: ShipmentOption[]) => {
    expect(options).toEqual(expected);
  });
});

Result:

That’s not what we were expecting! What happened? 😥 Our test just ran through and did not wait for the assertion. Oh, wait! There is an easy way to get it failing:

it('should return the delivery methods with address if Post is available', (done) => {
  const shipmentOptions$: Observable<ShipmentOption[]> = shipmentService.getShipmentOptions(2);
  const expected: ShipmentOption[] = [
    {deliveryMethod: 'Pickup'},
    {deliveryMethod: 'Post', address},
  ];

  shipmentOptions$.subscribe((options: ShipmentOption[]) => {
    expect(options).toEqual(expected);
    done();
  });
});

We use the done callback to tell our tests when we are really done. Result:

Test result

Our test runs into a timeout and fails. That’s good. But our test runs for 5.072 secs! That’s not cool. Not for TDD and not on any Jenkins. And what if we forget to add done()? So, this variant is not ideal to test observables.

❗ Remark: You should of course always try to first get your tests failing. Then you can be more confident that they test the right thing and catch the wrong values.

2 – Using BehaviourSubject

Our dependencies (the two services) just return an Observable. And in our test, we somehow want to tell the dependencies what to return. An rxjs Subject is a special type of observable. Can’t we just use the next() method of the Subject to pass the values into it that are then emitted when we want? Let’s try:

const deliveryMethodsOutput$ = new Subject<DeliveryMethod[]>();

beforeEach(() => {
  deliveryMethodService = {
    getDeliveryMethods: (): Observable<DeliveryMethod[]> => deliveryMethodsOutput$.asObservable()
  } as unknown as DeliveryMethodService;

  customerService = {
    getCustomerAddresses: (): Observable<Address[]> => of([address])
  } as unknown as CustomerService;

  shipmentService = new ShipmentService(deliveryMethodService, customerService);
});

The deliveryMethodsOutput$ is a Subject and returned as observable for getDeliveryMethods(). In our test, we can control the values to emit by calling next. Here we want ‘Pickup’ and ‘Post’ back:

it('should return the delivery methods with address if Post is available', () => {
  deliveryMethodsOutput$.next(['Pickup', 'Post']);
  const shipmentOptions$: Observable<ShipmentOption[]> = shipmentService.getShipmentOptions(customerId);
  const expected: ShipmentOption[] = [
    {deliveryMethod: 'Pickup'},
    {deliveryMethod: 'Post', address},
  ];

  shipmentOptions$.subscribe((options: ShipmentOption[]) => {
    expect(options).toEqual(expected);
  });
});

And here as well:

it('should return the delivery methods without address if no address returned', () => {
  deliveryMethodsOutput$.next(['Pickup', 'Post']);
  customerService.getCustomerAddresses = () => of([]);

  const shipmentOptions$: Observable<ShipmentOption[]> = shipmentService.getShipmentOptions(customerId);
  const expected: ShipmentOption[] = [
    {deliveryMethod: 'Pickup'},
    {deliveryMethod: 'Post'},
  ];

  shipmentOptions$.subscribe((options: ShipmentOption[]) => {
    expect(options).toEqual(expected);
  });
});

Result:

Test successful

Cool, it seems to work. Let’s add a third test where we only want to return ‘Pickup’ as delivery method. I add this test between the other two:

it('should return the delivery methods without fetching the address if only pickup is available', () => {
  deliveryMethodsOutput$.next(['Pickup']);
  customerService.getCustomerAddresses = () => of([]);

  const shipmentOptions$: Observable<ShipmentOption[]> = shipmentService.getShipmentOptions(customerId);
  const expected: ShipmentOption[] = [
    {deliveryMethod: 'Pickup'},
  ];

  shipmentOptions$.subscribe((options: ShipmentOption[]) => {
    expect(options).toEqual(expected);
  });
});

Result:

Test result: 2 failed, 1 passed

Something went wrong. We went from “2 SUCCESS” to “2 FAILED, 1 SUCCESS” by adding 1 Test? Tests should never depend on each other! Obviously with the solution above we have a shared state between tests (deliveryMethodsOutput$). Especially when tests have different timings or run in parallel this leads to problems that are hard to analyze. So, also using a Subject does not seem ideal.

3 – rxjs-marbles

Isn’t there an easy way to test observables? Ideally with a possibility to simulate different timings and multiple emissions? There is one! ✅ The rxjs library itself offers us a way to test observables. They provide a TestScheduler and some methods that allow defining inputs and outputs in a clear way. We are making use of cold(…) which is an observable whose subscription starts when the test begins. For the assertion expectObservable helps us:

it('should return the delivery methods with address if Post is available', () => {
  scheduler.run(({cold, expectObservable}) => {
    deliveryMethodService.getDeliveryMethods = () => cold('a|', {a: ['Pickup', 'Post']});
    customerService.getCustomerAddresses = () => cold('-a|', {a: [address]});

    const shipmentOptions$: Observable<ShipmentOption[]> = shipmentService.getShipmentOptions(customerId);
    const expected: ShipmentOption[] = [
      {deliveryMethod: 'Pickup'},
      {deliveryMethod: 'Post', address},
    ];

    expectObservable(shipmentOptions$).toBe('-a|', {a: expected});
  });
});

Result:

We could for example also have test cases where the customer address request should not be executed (when only pickup is available):

it('should return the delivery methods without fetching the address if only pickup is available', () => {
  scheduler.run(({cold, expectObservable}) => {
    deliveryMethodService.getDeliveryMethods = () => cold('a|', {a: ['Pickup']});
    customerService.getCustomerAddresses = () => cold('#');

    const shipmentOptions$: Observable<ShipmentOption[]> = shipmentService.getShipmentOptions(customerId);
    const expected: ShipmentOption[] = [
      {deliveryMethod: 'Pickup'},
    ];

    expectObservable(shipmentOptions$).toBe('a|', {a: expected});
  });
});

The ‘#’ denotes an error in the observable. So, this test would run into an error if the call is executed (probably toHaveBeenCalledTimes(…) is a better way to test how many times something was executed). What about the EMPTY case from above? What happens if there comes nothing back when asking for shipment options?

if (customerId === 2) {
  return EMPTY;
}

Result:

Test result for EMPTY case

We see the failure. Because we exactly tell in the test what kind of observable we expect and at what timeframe we expect it (the ‘-‘ in ‘-a|’ stands for 1 passed frame in virtual time). Additionally, we see it already after a few milliseconds and not after more than 5 seconds. Developers and CI infrastructures are happy! 😎

Conclusion and Take-Aways

There are different ways to mock observables in your tests. It is important to understand what each of the variants offers and where the problems could be located. In my opinion, rxjs-marbles should always be the way to go. It offers a handy, declarative option to define observables and expectations.

💡 Takeaways:

  • Make sure your tests are failing first, then you can be more confident they actually test the right thing.
  • Don’t introduce dependencies between tests.
  • Use rxjs-marbles to safely test your observables, especially if timing is relevant or multiple emitted values should be tested.
Avatar

About the author

Ronnie Schaniel

Ronnie Schaniel is a Software Engineer with 10+ years experience in industry and academia. Currently he is working in a larger enterprise as Full-Stack Software Engineer with Angular and Java. He is passionate about various software engineering topics, especially Agile and Software Architecture. Ronnie writes blog articles on Medium and you can also connect with him on Twitter.

Join TestProject Community

Get full access to the world's first cloud-based, open source friendly testing community. Enjoy TestProject's end-to-end test automation Platform, Forum, Blog and Docs - All for FREE.

Join Us Now  

Leave a Reply

popup image

Test, Deploy & Debug in < 1 hr

Leverage a cross platform open source automation framework for web & mobile testing using any language you prefer, and benefit from built-in dashboards & reports. Free & open source.
Get Started
FacebookLinkedInTwitterEmail