I am currently using a autocomplete mechanism (typeahead) of ngBootstrap. Now I want to unit test if a method is called on every sequence of an input event. The error on my test case is currently: Cannot read property 'pipe' of undefined
<input id="locationEdit" type="text" class="form-control"
[(ngModel)]="user.location" name="location [ngbTypeahead]="search"/>
public ngOnInit() {
this.search = (text$: Observable<string>) =>
text$.pipe(
tap(() => {
this.isSearching = true;
this.searchFailed = false;
}),
debounceTime(750),
distinctUntilChanged(),
switchMap(term =>
this.cityService.getLocation(term).pipe(
tap((response) => {
this.searchFailed = response.length === 0;
this.isSearching = false;
})))
);
}
it('should call spy on city search', fakeAsync(() => {
component.user = <User>{uid: 'test', username: 'mleko', location: null, description: null};
const spy = (<jasmine.Spy>cityStub.getLocation).and.returnValue(of['München Bayern']);
fixture.detectChanges();
const compiled: DebugElement = fixture.debugElement.query(By.css('#locationEdit'));
compiled.nativeElement.value = 'München';
compiled.nativeElement.dispatchEvent(new Event('input'));
tick(1000);
fixture.detectChanges();
expect(spy).toHaveBeenCalled();
}));
Can someone help me to mock this.search properly?
By the awesome suggestion of @dmcgrandle I don't need to render the HTML and simulate an input-event, to check if the typeahead is working. I rather should make an Observable, which emits values and assigns it to the function. One approach is:
it('should call spy on city search', fakeAsync(() => {
const spy = (<jasmine.Spy>cityStub.getLocation).and.returnValue(of['München Bayern']);
component.ngOnInit();
const textMock = of(['M', 'Mün', 'München']).pipe(flatMap(index => index));
component.search(textMock);
tick();
expect(spy).toHaveBeenCalled();
}));
But the problem still is, component.search
does not call the spy. Within the search function in the switchMap
operator I added a console.log
to see if value are emitted from the function. But that is not the case.
I don't think you actually want to call any ngBootstrap code during your test - after all you want to unit test your code, not theirs. :)
Therefore I would suggest mocking the user actually typing by setting up a timed Observable of your own, and calling your function with it. Perhaps mock sending a character every 100ms. Something like this:
it('should call spy on city search', fakeAsync(() => {
component.user = <User>{uid: 'test', username: 'mleko', location: null, description: null};
// Change next line depending on implementation of cityStub ...
const spy = spyOn(cityStub, 'getLocation').and.returnValue(of('München Bayern'));
fixture.detectChanges();
let inputTextArray = ['M', 'Mü', 'Mün', 'Münc', 'Münch', 'Münche', 'München'];
let textMock$ : Observable<string> = interval(100).pipe(take(7),map(index => inputTextArray[index]));
component.search(textMock$);
tick(1000);
expect(spy).toHaveBeenCalled();
}));
Update:
I put together a stackblitz here to test this out: https://stackblitz.com/edit/stackoverflow-question-52914753 (open app folder along the left side and click on my.component.spec.ts to see the test file)
Once I got it in there, it was obvious what was wrong - the observable was not being subscribed to because the subscription seems to be done by ngBootstrap, so for testing we need to subscribe explicitly. Here is my new suggested spec (taken from the stackblitz):
it('should call spy on city search', fakeAsync(() => {
const cityStub = TestBed.get(CityService);
const spy = spyOn(cityStub, 'getLocation').and.returnValue(of('München Bayern'));
fixture.detectChanges();
let inputTextArray = ['M', 'Mü', 'Mün', 'Münc', 'Münch', 'Münche', 'München'];
let textMock$ : Observable<string> = interval(100).pipe(take(7),map(index => inputTextArray[index]));
component.search(textMock$).subscribe(result => {
expect(result).toEqual('München Bayern');
});
tick(1000);
expect(spy).toHaveBeenCalled();
}));
Please try moving the observable inside of the service:
Component:
this.cityService.text$.pipe
Service:
export class CityService {
private _subject = null;
text$ = null;
constructor(private _httpClient: HttpClient) {
this.init();
}
init() {
this._subject = new BehaviorSubject<any>({});
this.text$ = this._subject.asObservable();
}
I can expand on my answer if you need more details.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With