Skip to content

Testing with TestScheduler

RxJS's TestScheduler is a powerful tool for accurately testing time-based operators. This chapter systematically explains how to test utilizing TestScheduler.

What is TestScheduler?

Normally, Observable works in a time-dependent manner. For example, delay() and debounceTime() are operators that wait for a certain time. Since it is inefficient to actually wait in testing, TestScheduler is a mechanism to test immediately using virtual time.

TIP

TestScheduler uses "virtual time", so there is no need to wait for real time.

Basic configuration of TestScheduler

This is the basic test configuration using TestScheduler.

ts
import { TestScheduler } from 'rxjs/testing';
import { describe, it, beforeEach, expect } from 'vitest';

describe('TestScheduler basics', () => {
  let testScheduler: TestScheduler;

  beforeEach(() => {
    testScheduler = new TestScheduler((actual, expected) => {
      expect(actual).toEqual(expected);
    });
  });

  it('Simple test', () => {
    testScheduler.run(({ cold, expectObservable }) => {
      const source$ = cold('--a--b--c|');
      const expected =  '--a--b--c|';

      expectObservable(source$).toBe(expected);
    });
  });
});
  • cold(): Create a Cold Observable where the stream starts independently for each subscription
  • hot(): Create a Hot Observable where the stream is already in progress
  • expectObservable(): verify the Observable's output with marble notation

Cold Observable and Hot Observable

TypeCharacteristicsUse
Cold ObservableData flow from the beginning for each subscriptionHTTP request, etc.
Hot ObservableData flow has already started and is shared with subscribersUser events, WebSockets, etc.

Basics of marble notation

Marble notation is a method of representing the passage of time in Observable as a string.

SymbolMeaning
-Time elapsed (one frame)
a, b, cvalue issued
``
#Error
() Multiple values issued simultaneously (multiple events)

Example

--a--b--c|    // a after 2 frames, then b, c, complete

Example test with virtual time

Testing with debounceTime

Test the debounceTime operator using virtual time.

ts
import { describe, it, expect, beforeEach } from 'vitest';
import { debounceTime, map } from 'rxjs';
import { TestScheduler } from 'rxjs/testing';

describe('Testing with virtual time', () => {
  let testScheduler: TestScheduler;

  beforeEach(() => {
    testScheduler = new TestScheduler((actual, expected) => {
      expect(actual).toEqual(expected);
    });
  });

  it('Test debounceTime operator', () => {
    testScheduler.run(({ cold, expectObservable }) => {
      const source$ = cold('--a--b----c|');
      const result$ = source$.pipe(
        debounceTime(20),
        map(x => x.toUpperCase())
      );
      const expected =    '-----------(C|)';  // ← This!

      expectObservable(result$).toBe(expected, { B: 'B', C: 'C' });
    });
  });
});

Test error handling

Test the behavior when an error occurs.

ts
import { describe, it, expect, beforeEach } from 'vitest';
import { catchError} from 'rxjs';
import { TestScheduler } from 'rxjs/testing';
import { of } from 'rxjs';

describe('Error handling test', () => {
  let testScheduler: TestScheduler;

  beforeEach(() => {
    testScheduler = new TestScheduler((actual, expected) => {
      expect(actual).toEqual(expected);
    });
  });

  it('Catch error with catchError', () => {
    testScheduler.run(({ cold, expectObservable }) => {
      const source$ = cold('--a--#');
      const result$ = source$.pipe(
        catchError(() => of('X'))
      );

      const expected =    '--a--(X|)';

      expectObservable(result$).toBe(expected);
    });
  });
});

Summary

  • TestScheduler allows testing without waiting for real time
  • Understand the difference between Cold/Hot Observable and use it differently
  • Visualize the passage of time using marble notation
  • Test even complex asynchronous streams with precision

[!NEXT] Next, we will learn more advanced marble testing (customization of marble strings and combinations of multiple streams).

Released under the CC-BY-4.0 license.