33

How can I test Observables with Jest?

I have an Observable that fires ~every second, and I want to test that the 1st event is correctly fired, before jest times out.

const myObservable = timer(0, 1000); // Example here

it('should fire', () => {
  const event = myObservable.subscribe(data => {
    expect(data).toBe(0);
  });
});

This test passes, but it also passes if I replace with toBe('anything'), so I guess I am doing something wrong.

I tried using expect.assertions(1), but it seems to be only working with Promises.

skyboyer
  • 22,209
  • 7
  • 57
  • 64
jeanpaul62
  • 9,451
  • 13
  • 54
  • 94

6 Answers6

34

There are some good examples in the Jest documentation about passing in an argument for the test. This argument can be called to signal a passing test or you can call fail on it to fail the test, or it can timeout and fail.

https://jestjs.io/docs/en/asynchronous.html

https://alligator.io/testing/asynchronous-testing-jest/

Examples

Notice I set the timeout to 1500ms

const myObservable = timer(0, 1000); // Example here

it('should fire', done => {
  myObservable.subscribe(data => {
    done();
  });
}, 1500); // Give 1500ms until it fails

Another way to see if it fails using setTimeout

const myObservable = timer(0, 1000); // Example here

it('should fire', done => {
  myObservable.subscribe(data => {
    done();
  });

  // Fail after 1500ms
  setTimeout(() => { done.fail(); }, 1500);
}, timeToFail);
CTS_AE
  • 12,987
  • 8
  • 62
  • 63
  • 2
    Observables and async are very different concepts. Treating an observable as async works for simple use cases (like the one highlighted here). As soon as things become complicated your going to struggle to get these tests working correctly. That's why [`TestScheduler`](https://stackoverflow.com/a/68470597/542251) exists – Liam Jul 21 '21 at 13:43
23

My preferred way to test observables, without fake timers and timeouts, is to async, await and use resolves or rejects on an expected converted promise.

it('should do the job', async () => {
    await expect(myObservable
      .pipe(first())
      .toPromise())
      .resolves.toEqual(yourExpectation);
});

Update:

In Rxjs 7 and onwards, you can use lastValueFrom or firstValueFrom for the promise convertion

it('should do the job', async () => {
    await expect(lastValueFrom(myObservable))
      .resolves.toEqual(yourExpectation);
});

ginalx
  • 1,905
  • 1
  • 15
  • 19
10
test('Test name', (done) => {
  service.getAsyncData().subscribe((asyncData)=>{
    expect(asyncData).toBeDefined();
       done();
    })
  });
})
Yoav Schniederman
  • 5,253
  • 3
  • 28
  • 32
  • I used this scheme, but it has the problem of not calling done() when the expectation fails, resulting in a rather messy output. CTS_AE's solution is a way to solve this. – PhiLho Jun 03 '22 at 07:11
9

the correct way to test any RXJS observable (Jest or no) is to the use the TestScheduler in rxjs/testing:

e.g.:

import { TestScheduler } from 'rxjs/testing';
import { throttleTime } from 'rxjs/operators';
 
const testScheduler = new TestScheduler((actual, expected) => {
  // asserting the two objects are equal - required
  // for TestScheduler assertions to work via your test framework
  // e.g. using chai.
  expect(actual).deep.equal(expected);
});
 
// This test runs synchronously.
it('generates the stream correctly', () => {
  testScheduler.run((helpers) => {
    const { cold, time, expectObservable, expectSubscriptions } = helpers;
    const e1 = cold(' -a--b--c---|');
    const e1subs = '  ^----------!';
    const t = time('   ---|       '); // t = 3
    const expected = '-a-----c---|';
 
    expectObservable(e1.pipe(throttleTime(t))).toBe(expected);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });
});

From the RXJS marble testing testing docs.

Trying to convert observables, etc. into promises works fine if you have a simple observable. As soon as things become more complicated you are going to struggle without using marble diagrams and the correct testing library.

CTS_AE
  • 12,987
  • 8
  • 62
  • 63
Liam
  • 27,717
  • 28
  • 128
  • 190
1

Here's an Angular approach using fakeAsync

Suppose we have a FooService with an Observable closed$ that emit every time we call the dismiss() method of the service.

@Injectable()
export class FooService {
    private closeSubject$ = new Subject<void>();
    public close$ = this.closeSubject$.asObservable();
    
    public dismiss() {
        this.closeSubject$.next();
    }
}

Then we can test the close$ emission like this

describe('FooService', () => {
    let fooService: FooService;

    beforeEach(() => {
        TestBed.configureTestingModule({
            providers: [FooService]
        });
        fooService= TestBed.inject(FooService);
    });

    it('should emit a close event upon calling dismiss()', fakeAsync(() => {
        const callbackSpy = jest.fn();
        fooService.close$.subscribe(() => {
            callbackSpy();
        });
        fooService.dismiss();
        tick();
        expect(callbackSpy).toHaveBeenCalledTimes(1);
    }));
});
Johan Aspeling
  • 765
  • 1
  • 13
  • 38
WSD
  • 3,243
  • 26
  • 38
0

There are 2 approaches mentioned above

  1. Taking argument done in our test and call it when we have tested.
  2. Convert our observable to promise using firstValueFrom(myObs) or lastValueFrom(myObs). and use async await with them...

If we have multiple observables to test then we have to nest the observables in our test as we can call done() only once. In that case async await approach can come handy. In this example when we call filter Customer all three observables emits values so we have to test all of them.

it('Filter Customers based on Producers- Valid Case Promise way ',async()=>{
    
    service.filterCustomers('Producer-1');

    await expect(firstValueFrom(service.customers$)).resolves.toEqual(['Customer-1']);

    await firstValueFrom(service.customers$).then((customers:string[])=>{
      expect(customers).toEqual(['Customer-1']);
      expect(customers.length).toBe(1);
    })

    await expect(firstValueFrom(service.products$)).resolves.toEqual([]);
    await expect(firstValueFrom(service.types$)).resolves.toEqual([]);

  }).