Как выполнить модульное тестирование FormControl в 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. Самая близкая к этому статья - статья о запросах Mocking Http, которая аналогична концепции, так как они возвращают Observables, но я не думаю, что она применима полностью.
Для справки я использую Angular RC5, запускаю тесты с Karma, а фреймворк - Jasmine.
1 ответ
Для начала давайте разберемся с некоторыми общими проблемами при тестировании асинхронных задач в компонентах. Когда мы тестируем асинхронный код, который тест не контролирует, мы должны использовать 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(1000)
,
Это (fakeAsync
) полезная функция, но проблема в том, что когда мы используем templateUrl
в нашем @Component
s, это делает вызов XHR, и вызовы XHR не могут быть сделаны в fakeAsync
, Существуют ситуации, когда вы можете смоделировать сервис, чтобы сделать его синхронным, как упоминалось в этом посте, но в некоторых случаях это просто невозможно или просто слишком сложно. В случае форм это просто неосуществимо.
По этой причине при работе с формами я склонен помещать шаблоны в template
вместо внешнего templateUrl
и разбить форму на более мелкие компоненты, если они действительно большие (просто чтобы в файле компонента не было огромной строки). Единственный другой вариант, который я могу придумать, - это использовать setTimeout
внутри теста, чтобы пропустить асинхронную операцию. Это вопрос предпочтений. Я просто решил использовать встроенные шаблоны при работе с формами. Это нарушает согласованность структуры моего приложения, но мне не нравится setTimeout
решение.
Теперь, что касается фактического тестирования форм, лучшим источником, который я нашел, было просто посмотреть на тесты интеграции исходного кода. Вы захотите изменить тег на версию Angular, которую вы используете, так как основная ветвь по умолчанию может отличаться от используемой вами версии.
Ниже приведены несколько примеров.
При тестировании входных данных вы хотите изменить входное значение на nativeElement
и отправить input
использование события 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
будет новое значение.
Если вы хотите использовать 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.
})
})