Reputation: 7103
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
in HTML always has default state. The issue is solely related to HTML.
My attempted tries:
fixture.detectChanges();
- Added this line to almost every second line (to such extreme) in both beforeEach()
and in it
test case but nothing was changedcomponent.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)fixture.whenRenderingDone().then(async () => { <code> });
in entire test case (perhaps HTML was not rendered by the time console.log()
lines came up)setTimeout()
, with same reasoningMy other thoughts are also:
declarations
, imports
, etc.?async
pipes (although they work for subscribe()
)If something is missing, let me know. Thank you in advance.
Upvotes: 1
Views: 1094
Reputation: 18869
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
});
});
});
Upvotes: 2