简体   繁体   中英

Angular Async pipe does not properly update in Unit Tests

I'm facing an issue with rendering updating Observables through async pipe in HTML while writing unit tests.

The idea is that I want test not just the component but whether child components are both rendered and have correct Inputs.

This is the minimal example that the issue occurs:

<ng-container *ngFor="let plan of plans$ | async">
  <child-component [plan]="plan"></child-component>
</ng-container>

Visible plans: {{ plans$ | async | json }}

The minimal example of Component:

export class RecommendationsComponent implements OnInit {
  public plans$: Observable<Plan[]>;

  constructor(private readonly _store: Store<State>) {
    this.plans$ = this._store.pipe(select(selectRecommendationsPayload));
  }

  public ngOnInit(): void {
    this.getRecommendations(); // Action dispatch, state is filled with data
  }
}

Unit test for this module/component:

describe('Recommendations', () => {
  let component: RecommendationsComponent;
  let fixture: ComponentFixture<RecommendationsComponent>;
  let store: Store<any>;
  let mockStore: MockStore<any>;
  let actions$: ReplaySubject<any> = new ReplaySubject<any>();

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [RecommendationsComponent],
      imports: [RouterTestingModule.withRoutes([]), HttpClientTestingModule],
      providers: [
        MockStore,
        provideMockStore({ initialState: initialStateMock }),
        provideMockActions(() => actions$),
      ],
    });

    store = TestBed.inject(Store);
    mockStore = TestBed.inject(MockStore);

    fixture = TestBed.createComponent(RecommendationsComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should successfully retrieve and handle plans', () => {
    recommendationsService.getRecommendations = jasmine.createSpy().and.returnValue(of(plans)); // Mock BE response with non-empty data

    component.plans$.subscribe(plans => {
      console.log(plans);
      console.log(fixture.debugElement);
      // A few expect() based on state and HTML...
      // This fires since all logic starts on ngOnInit() lifecycle
    });
  });
});

While real code and console.log(plans); in unit test show correct data, for some reason the plans$ | async plans$ | async in HTML always has default state. The issue is solely related to HTML.

My attempted tries:

  1. Add fixture.detectChanges(); - Added this line to almost every second line (to such extreme) in both beforeEach() and in it test case but nothing was changed
  2. Hardcoded with component.plans$ = of([ { name: 'name' } as any ]);in it test case (I was wondering if this had something to do with Store/MockStore but even hardcoded value appears to be not working in HTML)
  3. Use fixture.whenRenderingDone().then(async () => { <code> }); in entire test case (perhaps HTML was not rendered by the time console.log() lines came up)
  4. Similar to the third, I also tried with setTimeout() , with same reasoning

My other thoughts are also:

  1. I have missed something in declarations , imports , etc.?
  2. MockStore/Store does not properly trigger changes to async pipes (although they work for subscribe() )

If something is missing, let me know. Thank you in advance.

What is strange to me is that you have a handle on both store and mockStore .

I think you should only use one. I don't have much experience with mockStore so I will try the actual store. Try doing integration testing as shown here . With integration testing we have the actual store and not a mock store.

describe('Recommendations', () => {
  let component: RecommendationsComponent;
  let fixture: ComponentFixture<RecommendationsComponent>;
  let store: Store<any>;
  let actions$: ReplaySubject<any> = new ReplaySubject<any>();

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [RecommendationsComponent],
      imports: [
         RouterTestingModule.withRoutes([]), 
         HttpClientTestingModule,
         StoreModule.forRoot({
           // Pay attention here, make sure this is provided in a way
           // where your selectors will work (make sure the structure is
           // good)
           recommendations: recommendationsReducer,
         })
      ],
    });

    store = TestBed.inject(Store);
    // load the recommendations into the store by dispatching
    store.dispatch(new loadRecommendations([]));
    fixture = TestBed.createComponent(RecommendationsComponent);
    component = fixture.componentInstance;
    // see your state here, make sure the selector works
    store.subscribe(state => console.log(state));
    // any time you want to change plans, do another dispatch
    store.dispatch(new loadRecommendations([/* add stuff here */]));
    // the following above should make plans$ emit every time
    fixture.detectChanges();
  });
  

 // !! -- The rest is up to you from now on but what I presented above
 // should help in getting new plans$ with the async pipe !!-

  it('should successfully retrieve and handle plans', () => {
    recommendationsService.getRecommendations = jasmine.createSpy().and.returnValue(of(plans)); // Mock BE response with non-empty data

    component.plans$.subscribe(plans => {
      console.log(plans);
      console.log(fixture.debugElement);
      // A few expect() based on state and HTML...
      // This fires since all logic starts on ngOnInit() lifecycle
    });
  });
});

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM