How to test BehaviourSubject next function?

user8408326 picture user8408326 · Aug 3, 2017 · Viewed 13.5k times · Source

I am trying to test a service that is using a BehaviourSubject, however I am not exactly sure how. Here is my service:

export class ProductService {
  private changes$: BehaviorSubject<User[]> = new BehaviorSubject([]);

  get products() { return this.changes$.asObservable() as Observable<any>; }

  getProducts() {
    this.http.get(...)
      .subscribe((products) => {
        this.changes$.next(products);
    }
  }

  filterById(id: number) {
    let products = this.products$.getValue().filter(...);
    // Maybe I will have some more filter logic here. 
    // I want to test that the filter logic is returning the correct product
    this.changes$.next(products);
  }
...
}

Here is my test. I want to be able to test the filter logic of my service. But since I am mocking the data, this.product$.getValue() will be empty and the filter will not run.

it('should filter the products and return the correct product)',
    inject([ProductService], (service: ProductService) => {
      const usersSpy = spyOnProperty(service, 'products', 'get').and.returnValue(Observable.of(mockGetResponse));
      service.products.subscribe((res) => {
        // How do I continue here?  
        // the `this.products$.getValue()` 
        // will not contain any items, 
        // because I am mocking the data. 
        service.findByUserId(1); 
      });
    }));

Answer

theMayer picture theMayer · May 30, 2018

When you're testing an observable sequence, it's important to keep in mind that the emitted values come later (asynchronously) (JavaScript is single-threaded, but you can treat it similarly to a separate thread). So, there's nothing explicitly wrong with trying to test them with mock data.

BehaviorSubject is a bit of a corner case, as it is programmed to emit some type of default value when subscribe() is called. Thus, you probably want to skip this value the first time around.

  it('should emit filtered products', async(inject([ProductService], (service: MockProductService) => {
    let testId = 'testId';  // you need to define this as appropriate
    let testProduct;        // you need to define this to be something that will be returned when the filter is set

    // subscribe, but skip the first as this is a BehaviorSubject and emits a default first value.
    service.filteredProducts$.skip(1).subscribe((o) => expect(o).toBe(testProduct));

    service.findByUserId(testId);
  })));

Writing a spy in this case doesn't seem to make a whole lot of sense, as all you're doing is testing the test code. If you're trying to test the service itself, you can stub the methods that reach back to the server, or you can write a mock component (which is what I usually do) that does this (and I usually make my mock component and real component share a common base class).