Angular 测试 - ngBootstraps typeahead
Angular testing - ngBootstraps typeahead
我目前正在使用 ngBootstrap 的自动完成机制(提前输入)。现在我想对输入事件的每个序列是否调用一个方法进行单元测试。目前我的测试用例的错误是:Cannot read property 'pipe' of undefined
Html:
<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;
})))
);
}
spec.ts
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();
}));
有人可以帮我正确模拟 this.search 吗?
编辑
根据@dmcgrandle 的绝妙建议,我不需要渲染 HTML 和模拟输入事件来检查预输入是否正常工作。我宁愿做一个 Observable,它发出值并将其分配给函数。一种方法是:
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();
}));
但问题还是,component.search
没有调用间谍。在 switchMap
运算符的搜索函数中,我添加了一个 console.log
来查看函数是否发出值。但事实并非如此。
我不认为你真的想在测试期间调用任何 ngBootstrap 代码——毕竟你想对你的代码进行单元测试,而不是他们的。 :)
因此,我建议通过设置您自己的定时 Observable 来模拟用户实际键入,并使用它调用您的函数。也许每 100 毫秒模拟发送一个字符。像这样:
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();
}));
更新:
我在这里整理了一个 stackblitz 来测试它:https://stackblitz.com/edit/Whosebug-question-52914753(打开左侧的应用程序文件夹,然后单击 my.component.spec.ts 查看测试文件)
一旦我把它放在那里,就很明显出了什么问题 - 没有订阅可观察对象,因为订阅似乎是由 ngBootstrap 完成的,所以为了测试我们需要明确订阅。这是我的新建议规范(取自 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();
}));
请尝试将 observable 移动到服务内部:
组件:
this.cityService.text$.pipe
服务:
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();
}
如果您需要更多详细信息,我可以扩展我的回答。
我目前正在使用 ngBootstrap 的自动完成机制(提前输入)。现在我想对输入事件的每个序列是否调用一个方法进行单元测试。目前我的测试用例的错误是:Cannot read property 'pipe' of undefined
Html:
<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;
})))
);
}
spec.ts
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();
}));
有人可以帮我正确模拟 this.search 吗?
编辑
根据@dmcgrandle 的绝妙建议,我不需要渲染 HTML 和模拟输入事件来检查预输入是否正常工作。我宁愿做一个 Observable,它发出值并将其分配给函数。一种方法是:
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();
}));
但问题还是,component.search
没有调用间谍。在 switchMap
运算符的搜索函数中,我添加了一个 console.log
来查看函数是否发出值。但事实并非如此。
我不认为你真的想在测试期间调用任何 ngBootstrap 代码——毕竟你想对你的代码进行单元测试,而不是他们的。 :)
因此,我建议通过设置您自己的定时 Observable 来模拟用户实际键入,并使用它调用您的函数。也许每 100 毫秒模拟发送一个字符。像这样:
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();
}));
更新:
我在这里整理了一个 stackblitz 来测试它:https://stackblitz.com/edit/Whosebug-question-52914753(打开左侧的应用程序文件夹,然后单击 my.component.spec.ts 查看测试文件)
一旦我把它放在那里,就很明显出了什么问题 - 没有订阅可观察对象,因为订阅似乎是由 ngBootstrap 完成的,所以为了测试我们需要明确订阅。这是我的新建议规范(取自 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();
}));
请尝试将 observable 移动到服务内部:
组件:
this.cityService.text$.pipe
服务:
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();
}
如果您需要更多详细信息,我可以扩展我的回答。