Issue with Angular nested observable & forkJoin failing in one scenario, while it functions correctly in another

UPDATE The simulation for OtherService was incorrect. You can check the detailed explanation on this answer.


I am trying to identify the issue in the test file that causes it to behave differently from the component. I am puzzled as to why the next: statement in the test is never executed while it always works in the component, even though they share the same code.

The main goal is to understand why this disparity exists, rather than attempting to refactor the service.

service:


@Injectable({
  providedIn: 'root'
})
export class MyService {

  strings = ["a","b","c"]

  method():Observable<string> {
     const subject = new Subject<string>();
     let observableArray: Array<Observable<string>> = [];
    
     strings.forEach((data: strings) => {
      observableArray.push(
        this._otherService
          .returnsAnotherObservable()
          .pipe(
            tap({
              next: () => {
                console.log("next") // works in both, test & component
                subject.next(data);
              },
            })
          )
         )
    });
      forkJoin(observableArray)
        .pipe(tap(() => subject.complete()))
        .subscribe();

      return subject;
  }
}

component


...

ngOnInit(): void {
    this._myService
      .method()
      .subscribe({
        next: (value: string) => {
          console.log(string) // it works
        },
        complete: () => {
          console.log("end")
        }
      });
  }

test


Class MockOtherService{
   returnsAnotherObservable():Observable<any> {
       // return of(true) will not work because is not async
       return of(true).pipe(delay(1000)) // now works
   }
}

describe('MyService', () => {
  let service: MyService;
  beforeEach(async () => {
    TestBed.configureTestingModule({
      providers: [{provide: OtherService, useClass: MockOtherService }],
      imports: [AppModule]
    });
    service = TestBed.inject(MyService);
  });
   it("should work", () => {

    servie.method()
      .subscribe({
        next: (value: string) => {
          console.log("value", value) // It never reachs here
        },
        complete: () => {
          console.log("end") // it jumps directly here    
        }
    });
   });
});

I've tried to play with fakeAsync & tick(). I Also change the forkJoin to zip without success.

One thig that worked was changing subject() for BehaviorSubject(), because yeah, it returns everything always.. which lead me to think that the next() calls are happening before the test has been subscribed(if that makes sense..) but it's the same for the component.

Thank you

Answer №1

Your code is at risk of encountering racing conditions due to a time gap between the start of forkJoin processing and the subscription to a "result sink".

In tests, your inner observables operate quickly (unlike HTTP calls, for example, and may even be synchronous).

yourMethod(){
       ///...
    
        forkJoin(observableArray)
        .pipe(tap(() => subject.complete()))
        .subscribe(); //time START, observables are HOT and doing their job
      //tick tack tick tack 
      //processing is happening, nobody is listening for the results....
      //tick tack .....
      return subject;       //tick tack tick tack ook we are returning the sink
  }

//caller

service.yourMethod() 
       //fork join already started
       //tick tack tick tack END
       .subscribe() // ok now lets subscribe to results that will come FROM NOW ON 

However, the results have already been processed, yet no one has listened to them.

To refactor the code into a more appropriate RxJS pipeline, consider using mergeAll/concatAll, eliminating the need for an additional result sink and avoiding the subscription to forkJoin by simply returning it.

With just a few lines of code, you can achieve the desired outcome:

return forkJoin(observableArray).pipe(
           switchMap(results=>of(...results))
       )

This approach will emit all values one by one (once the observable completes). If you prefer real-time results, use merge instead of forkJoin.


BONUS

I have provided a practical example that illustrates the issue you observed and demonstrates the correct approach https://stackblitz.com/edit/rxjs-ve3jst?file=index.ts

import { of, map, Observable,forkJoin,tap,Subject,merge } from 'rxjs';

of('World')
  .pipe(map((name) => `Hello, ${name}!`))
  .subscribe(console.log);

// Open the console in the bottom right to see results.

const strings = ["a","b","c"]

function howItShouldNotBeDone():Observable<string> {
  const subject = new Subject<string>();
  let observableArray: Array<Observable<string>> = [];
 
  strings.forEach((data: string) => {
   observableArray.push(
     of("value: "+data)
       .pipe(
         tap({
           next: () => {
            //  console.log("next") // works in both, test & component
             subject.next(data);
           },
         })
       )
      )
 });
   forkJoin(observableArray)
     .pipe(tap(() => subject.complete()))
     .subscribe();

   return subject;
}

howItShouldNotBeDone().subscribe({
  next: (value: string) => {
    console.log("nah it will never happen anyway", value) // It never reachs here
  },
  complete: () => {
    console.log("how it should not be done ends") // it jumps directly here    
  }
});

function howItCouldBeDone(){
  const subject = new Subject<string>();
  let observableArray: Array<Observable<string>> = strings.map(data=>of(data))
  return merge(...observableArray)
}

howItCouldBeDone().subscribe({
  next: (value: string) => {
    console.log("Got value!", value) // It never reachs here
  },
  complete: () => {
    console.log("good code ends") // it jumps directly here    
  }
});

Output:

> Hello, World!
> how it should not be done ends
> Got value! a
> Got value! b
> Got value! c
> good code ends

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Sign up for a feature that provides an observable exclusively within an if statement

There is an if clause in my code that checks for the presence of the cordova object in the window global object. If cordova is present, it will make a http request and return the default angular 2 http observable. If the application is in a web context wh ...

"The functionality for detecting changes in an Electron application has ceased to work after implementing zone.js version 0.9.1

Recently, I took over an Electron app originally built with Angular 6 and decided to update it to Angular 8. After following all the upgrade guides, I managed to get everything up and running smoothly - or so I thought. It wasn't until I started inter ...

What is the best way to test the output of HTML code in a unit test scenario

I am new to web development and testing, taking it slow. I have a website with a button that reveals information when clicked. How can I create a test case to ensure that the displayed output is correct? I need to verify that the text appears on the scre ...

Using TypeScript with Styled Components .attrs

I'm a bit perplexed about using the .attrs() function in conjunction with TypeScript. Let's consider the code snippet below: BottleBar.tsx: interface IBottleComponentProps { fill?: boolean } const BottleComponent = styled.div.attrs<IBottl ...

What is the best method for conducting comprehensive testing of all projects and libraries within NestJS (nx)?

Our NestJS project has been established with multiple libraries through Nx. We have successfully run tests on individual projects/libraries using the following command: npx nx test lib1 --coverage While this method works well, we are faced with numerous l ...

typescript persist zustand: typing your store

Running a simple store interface CartState { cart: { [id: string]: CartDto }; addItem: ({ id, image, name, price }: Omit<CartDto, "quantity">) => void; removeItem: (id: string) => void; reset: () => void; } export const use ...

refresh the React component without having to refresh the entire webpage

Hey there, I have a component with a function called "handleAvatarUpload". Currently, when there is a change, the entire page reloads instead of just the component. Is there a way to reload only the component without refreshing the whole page? import { us ...

Caught off guard by this promise: TypeError - Attempting to access a property that does not exist in my Ionic 2 application

I'm encountering an issue with native Facebook login that displays the following error message: Uncaught (in promise): TypeError: Cannot read property 'apply' of undefined https://i.sstatic.net/PDWze.jpg I have shared my entire project ...

NestJS Resolver Problem: Getting an Undefined Error

Could use a bit of assistance. I'm currently working on a mutation and encountering the following error: ERROR [ExceptionsHandler] Cannot read properties of undefined (reading 'entryUser') Here is the resolver code snippet: export class Us ...

Error: The property ɵfac cannot be redefined within angular

core.js:27478 Uncaught TypeError: Cannot redefine property: ɵfac at Function.defineProperty (<anonymous>) at addDirectiveFactoryDef (core.js:27478:1) at compileComponent (core.js:27361:1) at ɵ3$1 (core.js:27674:93) at TypeDecora ...

When trying to update a form field, the result may be an

Below is the code for my component: this.participantForm = this.fb.group({ occupation: [null], consent : new FormGroup({ consentBy: new FormControl(''), consentDate: new FormControl(new Date()) }) }) This is th ...

What is the best way to guarantee an Array filled with Strings?

Which is the proper way to define a potentially array of strings? Promise<Array<string>> Or Promise<string[]> ...

Using optional chaining with TypeScript types

I'm dealing with a complex data structure that is deeply nested, and I need to reference a type within it. The issue is that this type doesn't have its own unique name or definition. Here's an example: MyQuery['system']['error ...

Firestore TimeStamp.fromDate is not based on UTC timing

Does anyone have a solution for persisting UTC Timestamps in Firestore? In my Angular application, when I convert today's date to a Timestamp using the code below, it stores as UTC+2 (due to summer time in Switzerland). import {firebase} from ' ...

Angular routing is showing an undefined ID from the snapshot

When a user attempts to update a student, I pass in the student ID. The update successfully redirects to the updateStudent page and displays the student ID in the browser link. However, within my app component, it shows as undefined. Student View componen ...

button listener event

Is there a way to verify if a button has been selected? <ul> <li *ngFor="let p of arrray; let i = index" > <button class="btn btn-success" (click)="onLoveIt(i)" >Love it!</button> &nbsp; </li> </ul> ...

Navigating through Angular to access a component and establishing a data binding

I am looking for the best way to transition from one component to another while passing data along with it. Below is an example of how I currently achieve this: this.router.navigate(['some-component', { name: 'Some Name' }]); In Some ...

Running your Angular application on a Node server: Step-by-step guide

I am looking to deploy my application using express on a Node server. This is the content of my server.js file: var express = require('express'); var path = require('path'); var app = express(); app.get('/', (req, res) => ...

Is there a way to access service data within an Angular interceptor?

I'm trying to include my authentication token in the request header within my auth.interceptor.ts. The value of the authentication token is stored in auth.service.ts. Below is the code for auth.interceptor.ts @Injectable() export class AuthIntercepto ...

What is the reason behind tsc (Typescript Compiler) disregarding RxJS imports?

I have successfully set up my Angular2 project using JSPM and SystemJS. I am attempting to import RxJS and a few operators in my boot.ts file, but for some reason, my import is not being transpiled into the final boot.js output. // boot.ts import {Observa ...