[英]How to unit test a FormControl in Angular2
我正在測試的方法如下:
/**
* Update properties when the applicant changes the payment term value.
* @return {Mixed} - Either an Array where the first index is a boolean indicating
* that selectedPaymentTerm was set, and the second index indicates whether
* displayProductValues was called. Or a plain boolean indicating that there was an
* error.
*/
onPaymentTermChange() {
this.paymentTerm.valueChanges.subscribe(
(value) => {
this.selectedPaymentTerm = value;
let returnValue = [];
returnValue.push(true);
if (this.paymentFrequencyAndRebate) {
returnValue.push(true);
this.displayProductValues();
} else {
returnValue.push(false);
}
return returnValue;
},
(error) => {
console.warn(error);
return false;
}
)
}
正如您所看到的,paymentTerm是一個返回Observable的表單控件,然后對其進行訂閱並檢查返回值。
我似乎無法找到有關單元測試FormControl的任何文檔。 我最接近的是這篇關於模擬Http請求的文章,這是一個類似的概念,因為它們正在返回Observables,但我不認為它完全適用。
作為參考,我使用Angular RC5,使用Karma運行測試,框架是Jasmine。
首先讓我們通過測試組件中的異步任務來解決一些常見問題。 當我們測試測試不受控制的異步代碼時,我們應該使用fakeAsync
,因為它允許我們調用tick()
,這使得操作在測試時看起來是同步的。 例如
class ExampleComponent implements OnInit {
value;
ngOnInit() {
this._service.subscribe(value => {
this.value = value;
});
}
}
it('..', () => {
const fixture = TestBed.createComponent(ExampleComponent);
fixture.detectChanges();
expect(fixture.componentInstance.value).toEqual('some value');
});
當調用ngOnInit
,此測試將失敗,但Observable是異步的,因此在測試中(即expect
)中的同步調用不會及時設置該值。
為了解決這個問題,我們可以使用fakeAsync
和tick
來強制測試等待所有當前的異步任務完成,使測試看起來像是同步的。
import { fakeAsync, tick } from '@angular/core/testing';
it('..', fakeAsync(() => {
const fixture = TestBed.createComponent(ExampleComponent);
fixture.detectChanges();
tick();
expect(fixture.componentInstance.value).toEqual('some value');
}));
現在測試應該通過,因為Observable訂閱沒有意外延遲,在這種情況下我們甚至可以在tick tick(1000)
傳遞毫秒延遲。
這個( fakeAsync
)是一個很有用的功能,但問題是當我們在@Component
使用templateUrl
時,它會進行XHR調用,並且不能在fakeAsync
進行XHR調用 。 在有些情況下,你可以嘲笑的服務,使之同步,如中提到的情況下這篇文章 ,但在某些情況下,這只是不可行或太困難。 在表格的情況下,這是不可行的。
出於這個原因,在處理表單時,我傾向於將模板放在template
而不是外部的templateUrl
,如果它們非常大(只是在組件文件中沒有大字符串),則將表單分成更小的組件。 我能想到的唯一另一個選擇是在測試中使用setTimeout
來讓異步操作通過。 這是一個偏好問題。 我剛決定在使用表單時使用內聯模板。 它打破了我的應用程序結構的一致性,但我不喜歡setTimeout
解決方案。
現在,就表單的實際測試而言,我發現的最佳來源只是查看源代碼集成測試 。 您需要將標記更改為您正在使用的Angular版本,因為默認主分支可能與您使用的版本不同。
以下是一些例子。
在測試輸入時,您需要更改nativeElement
上的輸入值,並使用dispatchEvent
調度input
事件。 例如
@Component({
template: `
<input type="text" [formControl]="control"/>
`
})
class FormControlComponent {
control: FormControl;
}
it('should update the control with new input', () => {
const fixture = TestBed.createComponent(FormControlComponent);
const control = new FormControl('old value');
fixture.componentInstance.control = control;
fixture.detectChanges();
const input = fixture.debugElement.query(By.css('input'));
expect(input.nativeElement.value).toEqual('old value');
input.nativeElement.value = 'updated value';
dispatchEvent(input.nativeElement, 'input');
expect(control.value).toEqual('updated value');
});
這是從源集成測試中獲得的簡單測試。 下面是更多的測試示例,一個是從源代碼中獲取的,另外一個不是,只是為了顯示測試中沒有的其他方法。
對於您的特定情況,看起來您正在使用(ngModelChange)
,您在其中為onPaymentTermChange()
分配調用。 如果是這種情況,那么您的實現沒有多大意義。 (ngModelChange)
在值發生變化時已經吐出一些東西,但每次模型更改時都會訂閱。 你應該做的是接受更改事件發出的$event
參數
(ngModelChange)="onPaymentTermChange($event)"
每次更改時,您都會傳遞新值。 所以只需在您的方法中使用該值,而不是訂閱。 $event
將是新值。
如果你想使用valueChange
在FormControl
,則應該開始聽它在ngOnInit
,所以你只能預訂一次。 你會在下面看到一個例子。 我個人不會去這條路。 我會按照您的方式進行,但不是訂閱更改,只需接受更改中的事件值(如前所述)。
這是一些完整的測試
import {
Component, Directive, EventEmitter,
Input, Output, forwardRef, OnInit, OnDestroy
} from '@angular/core';
import { Subscription } from 'rxjs/Subscription';
import { TestBed, fakeAsync, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser/src/dom/debug/by';
import { getDOM } from '@angular/platform-browser/src/dom/dom_adapter';
import { dispatchEvent } from '@angular/platform-browser/testing/browser_util';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
class ConsoleSpy {
log = jasmine.createSpy('log');
}
describe('reactive forms: FormControl', () => {
let consoleSpy;
let originalConsole;
beforeEach(() => {
consoleSpy = new ConsoleSpy();
originalConsole = window.console;
(<any>window).console = consoleSpy;
TestBed.configureTestingModule({
imports: [ ReactiveFormsModule ],
declarations: [
FormControlComponent,
FormControlNgModelTwoWay,
FormControlNgModelOnChange,
FormControlValueChanges
]
});
});
afterEach(() => {
(<any>window).console = originalConsole;
});
it('should update the control with new input', () => {
const fixture = TestBed.createComponent(FormControlComponent);
const control = new FormControl('old value');
fixture.componentInstance.control = control;
fixture.detectChanges();
const input = fixture.debugElement.query(By.css('input'));
expect(input.nativeElement.value).toEqual('old value');
input.nativeElement.value = 'updated value';
dispatchEvent(input.nativeElement, 'input');
expect(control.value).toEqual('updated value');
});
it('it should update with ngModel two-way', fakeAsync(() => {
const fixture = TestBed.createComponent(FormControlNgModelTwoWay);
const control = new FormControl('');
fixture.componentInstance.control = control;
fixture.componentInstance.login = 'old value';
fixture.detectChanges();
tick();
const input = fixture.debugElement.query(By.css('input')).nativeElement;
expect(input.value).toEqual('old value');
input.value = 'updated value';
dispatchEvent(input, 'input');
tick();
expect(fixture.componentInstance.login).toEqual('updated value');
}));
it('it should update with ngModel on-change', fakeAsync(() => {
const fixture = TestBed.createComponent(FormControlNgModelOnChange);
const control = new FormControl('');
fixture.componentInstance.control = control;
fixture.componentInstance.login = 'old value';
fixture.detectChanges();
tick();
const input = fixture.debugElement.query(By.css('input')).nativeElement;
expect(input.value).toEqual('old value');
input.value = 'updated value';
dispatchEvent(input, 'input');
tick();
expect(fixture.componentInstance.login).toEqual('updated value');
expect(consoleSpy.log).toHaveBeenCalledWith('updated value');
}));
it('it should update with valueChanges', fakeAsync(() => {
const fixture = TestBed.createComponent(FormControlValueChanges);
fixture.detectChanges();
tick();
const input = fixture.debugElement.query(By.css('input')).nativeElement;
input.value = 'updated value';
dispatchEvent(input, 'input');
tick();
expect(fixture.componentInstance.control.value).toEqual('updated value');
expect(consoleSpy.log).toHaveBeenCalledWith('updated value');
}));
});
@Component({
template: `
<input type="text" [formControl]="control"/>
`
})
class FormControlComponent {
control: FormControl;
}
@Component({
selector: 'form-control-ng-model',
template: `
<input type="text" [formControl]="control" [(ngModel)]="login">
`
})
class FormControlNgModelTwoWay {
control: FormControl;
login: string;
}
@Component({
template: `
<input type="text"
[formControl]="control"
[ngModel]="login"
(ngModelChange)="onModelChange($event)">
`
})
class FormControlNgModelOnChange {
control: FormControl;
login: string;
onModelChange(event) {
this.login = event;
this._doOtherStuff(event);
}
private _doOtherStuff(value) {
console.log(value);
}
}
@Component({
template: `
<input type="text" [formControl]="control">
`
})
class FormControlValueChanges implements OnDestroy {
control: FormControl;
sub: Subscription;
constructor() {
this.control = new FormControl('');
this.sub = this.control.valueChanges.subscribe(value => {
this._doOtherStuff(value);
});
}
ngOnDestroy() {
this.sub.unsubscribe();
}
private _doOtherStuff(value) {
console.log(value);
}
}
至於關於異步行為的這個答案的第一部分,我發現你可以使用fixture.whenStable()
來等待異步任務。 因此無需僅使用內聯模板
it('', async(() => {
fixture.whenStable().then(() => {
// your expectations.
})
})
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.