更新 关于异步行为的第一部分,我发现你可以使用 fixture.whenStable()
来等待异步任务。因此不需要仅使用内联模板。
it ('' , async (() => {
fixture.whenStable ().then (() => {
})
})
首先,让我们来谈谈关于在组件中测试异步任务的一些常见问题。当我们测试异步代码时,由于测试不受控制,因此我们应该使用 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请求,而XHR请求不能在fakeAsync
中发出 。 有些情况下,您可以模拟服务使其同步,如此帖子 中所述,但在某些情况下,这就不可行或者太困难了。 在表单的情况下,根本不可行。
因此,当处理表单时,我倾向于将模板放在template
中,而不是外部的templateUrl
中,并且如果表单实在很大,我会将其拆分成较小的组件(仅为了不在组件文件中具有巨大的字符串)。 我能想到的唯一另一种选择是在测试中使用setTimeout
,以让异步操作通过。 这是个人偏好。 我决定在处理表单时使用内联模板。 这打破了我的应用程序结构的一致性,但我不喜欢setTimeout
的解决方案。
现在就实际测试表单而言,我找到最好的来源只是看源代码集成测试 。您需要将标签更改为您正在使用的Angular版本,因为默认的主分支可能与您正在使用的版本不同。
以下是一些示例。
在测试输入时,您要做的就是更改nativeElement
上的输入值,并使用dispatchEvent
分派一个 事件。例如
@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将是新值。
如果您确实希望在FormControl上使用valueChange,则应在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);
}
}