Angular unit test combineLatest
Asked Answered
C

2

6

I have started off with unit testing with Jest Framework in Angular since a while now. However i am stuck in a situation where I need to unit test combineLatest RxJS operator. My component looks like below.

Component:

public userData;
public productData;

constructor(
    private readonly userService: UserService,
    private readonly productService: ProductService
) {}

public ngOnInit() {
    this.initialize();
}

public initialize() {
    combineLatest([
        this.userService.getUserData(),
        this.productService.getProductData()
    ])
    .subscribe([userData, productData] => {
        this.userData = userData;
        this.productData = productData;
    });
}

I have mocked both my services and my unit test looks like below.

it('should initialize user and product data', fakeAsync(() => {
    spyOn(userService, 'getUserData');
    spyOn(productService, 'getProductData');
    component.initialize();
    tick();
    fixture.detectChanges();
    expect(userService.getUserData).toHaveBeenCalled();
    expect(productService.getProductData).toHaveBeenCalled();
    expect(component.userData).toBeDefined();
    expect(component.productData).toBeDefined();
}));

This test fails with Received: undefined. But the same kind of test works when working with a single observable.

Comprise answered 30/10, 2020 at 7:57 Comment(0)
C
1

I will try and answer the question in a different approach.

Lets take a look at your code, It is easier to test simple chunks of code rather that test long code.

It will be much easier to assign the variables to your properties. With that you can simply test the property

See Below improved code;

constructor(
    private readonly userService: UserService,
    private readonly productService: ProductService
) {}
userData: any;
productData: any;
userData$ = this.userService.getUserData().pipe(
  tap(userData => this.userData = userData)
);
productData$ = this.productService.getProductData().pipe(
  tap(productData = productData => this.productData = productData)
)

v$ = combineLatest([this.userData$, this.productData$]).pipe(
  map(([userData, productData]) => ({ userData, productData }))
)
initialize() {
  v$.subscribe()
}
ngOnInit() {
    this.initialize();
}

Back to your initial testing

Lets now try to test your initial code and understand why the test is not working. From the official documentation

When any observable emits a value, emit the last emitted value from each

Lets have a look at the below code

    spyOn(userService, 'getUserData');
    spyOn(productService, 'getProductData');

Does the above code emit any value? The answer is no, so the code this.userService.getUserData() and this.productService.getProductData() will not return any value hence no value will be emitted. combineLatest will hence not emit any value.

Solution

You will need to return an Observable from the spyOn() function, something like spyOn(userService, 'getUserData').and.returnValue(of({}));

Now the Observable will emit and the test will pass

Final proposal for a better code.

You may consider using async pipe to handle your subscription. With async pipe your code can be written as below

constructor(
    private readonly userService: UserService,
    private readonly productService: ProductService
) {}
userData$ = this.userService.getUserData()
productData$ = this.productService.getProductData()
v$ = combineLatest([this.userData$, this.productData$]).pipe(
  map(([userData, productData]) => ({ userData, productData }))
)

In your html

<ng-container *ngIf='v$ | async'>
  {{ v.userData | json }}
  {{ v.productData| json }}   
</ng-container>

In your test file you can have

it('should initialize user and product data', fakeAsync(() => {
    spyOn(userService, 'getUserData').and.returnValue(of({}));
    spyOn(productService, 'getProductData').and.returnValue(of({}));
    tick();
    fixture.detectChanges();
    component.v$.subscribe({
      next: (v) => {
        expect(v.userData).toBeDefined();
        expect(v.productData).toBeDefined();
      }
    })
}));

The above test simply checks that if we subscribe to v$, then the userData and productData are defined

Copolymer answered 30/10, 2020 at 8:57 Comment(0)
S
2

You can mock combineLatest in jest like this

/* Mock rxjs's combineLatest */
const rxjs = jest.requireActual('rxjs');
rxjs.combineLatest = jest.fn(() => of(<ADD_YOUR_MOCK_DATA_HERE>));
Update: you can also mock using spyOn
jest.spyOn(rxjs, 'combineLatest')
      .mockReturnValue(of(<ADD_YOUR_MOCK_DATA_HERE>));

Full test looks something like this

it('should initialize user and product data', () => {
  const userMockData = <ADD_MOCK_DATA>;
  const productMockData = <ADD_MOCK_DATA>;

  /* Mock rxjs's combineLatest */
  const rxjs = jest.requireActual('rxjs');
  rxjs.combineLatest = jest.fn(() => of([userMockData, productMockData]));
  
  //or mock using spyOn
  jest.spyOn(rxjs, 'combineLatest')
    .mockReturnValue(of([userMockData, productMockData]));

  jest.spyOn(userService, 'getUserData');
  jest.spyOn(productService, 'getProductData');

  component.initialize();

  expect(component.userData).toEqual(userMockData);
  expect(component.productData).toEqual(productMockData);
});
Shutin answered 5/9, 2022 at 4:40 Comment(2)
I'm getting TypeError: Cannot set property combineLatest of [object Object] which has only a getterGristle
@Gristle unfortunately for me requireActual returns any, so I tried to add type manually, I got the error but not the above one, I have added one more way to mock it using spyOn works fine for me even with typeShutin
C
1

I will try and answer the question in a different approach.

Lets take a look at your code, It is easier to test simple chunks of code rather that test long code.

It will be much easier to assign the variables to your properties. With that you can simply test the property

See Below improved code;

constructor(
    private readonly userService: UserService,
    private readonly productService: ProductService
) {}
userData: any;
productData: any;
userData$ = this.userService.getUserData().pipe(
  tap(userData => this.userData = userData)
);
productData$ = this.productService.getProductData().pipe(
  tap(productData = productData => this.productData = productData)
)

v$ = combineLatest([this.userData$, this.productData$]).pipe(
  map(([userData, productData]) => ({ userData, productData }))
)
initialize() {
  v$.subscribe()
}
ngOnInit() {
    this.initialize();
}

Back to your initial testing

Lets now try to test your initial code and understand why the test is not working. From the official documentation

When any observable emits a value, emit the last emitted value from each

Lets have a look at the below code

    spyOn(userService, 'getUserData');
    spyOn(productService, 'getProductData');

Does the above code emit any value? The answer is no, so the code this.userService.getUserData() and this.productService.getProductData() will not return any value hence no value will be emitted. combineLatest will hence not emit any value.

Solution

You will need to return an Observable from the spyOn() function, something like spyOn(userService, 'getUserData').and.returnValue(of({}));

Now the Observable will emit and the test will pass

Final proposal for a better code.

You may consider using async pipe to handle your subscription. With async pipe your code can be written as below

constructor(
    private readonly userService: UserService,
    private readonly productService: ProductService
) {}
userData$ = this.userService.getUserData()
productData$ = this.productService.getProductData()
v$ = combineLatest([this.userData$, this.productData$]).pipe(
  map(([userData, productData]) => ({ userData, productData }))
)

In your html

<ng-container *ngIf='v$ | async'>
  {{ v.userData | json }}
  {{ v.productData| json }}   
</ng-container>

In your test file you can have

it('should initialize user and product data', fakeAsync(() => {
    spyOn(userService, 'getUserData').and.returnValue(of({}));
    spyOn(productService, 'getProductData').and.returnValue(of({}));
    tick();
    fixture.detectChanges();
    component.v$.subscribe({
      next: (v) => {
        expect(v.userData).toBeDefined();
        expect(v.productData).toBeDefined();
      }
    })
}));

The above test simply checks that if we subscribe to v$, then the userData and productData are defined

Copolymer answered 30/10, 2020 at 8:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.