Angular にはいくつかのフォームの実装方法がありますが、この章では基本的な Template driven Form のテスト方法を紹介します。
公式ドキュメント Template-driven Forms
https://angular.io/guide/forms#template-driven-forms
この記事は以下のテストコードを参考にしています。
Angular v5.2.8 template_integration_spec.ts
https://github.com/angular/angular/blob/5.2.8/packages/forms/test/template_integration_spec.ts
コンポーネントの作成
まずテストを書くためのコンポーネントを作成します。
$ ng generate component sample2
作成した空のコンポーネントに以下のようなテンプレートとプロパティを設定します。
HTML要素は前章の Sample1Component と同じですが、要素全体を <form>
タグで囲んでいるところに違いがあります。
// sample2.component.ts
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-sample2',
templateUrl: './sample2.component.html',
styleUrls: ['./sample2.component.css']
})
export class Sample2Component implements OnInit {
id: number;
name: string;
agreement: boolean;
gender: string;
works = [
{ id: 'employee', label: '会社員' },
{ id: 'student', label: '学生' },
{ id: 'other', label: 'その他' },
];
work: { [key: string]: string };
note: string;
...
}
// sample2.component.html
<form>
<span name="id">{{id}}</span>
<input name="name" type="text" [(ngModel)]="name">
<input type="checkbox" name="agreement" [(ngModel)]="agreement">
<input type="radio" name="gender" value="male" [(ngModel)]="gender">男
<input type="radio" name="gender" value="female" [(ngModel)]="gender">女
<select name="work" [(ngModel)]="work">
<option *ngFor="let item of works" [ngValue]="item">{{item.label}}</option>
</select>
<textarea name="note" [(ngModel)]="note"></textarea>
</form>
// app.module.ts
+ import { FormsModule } from '@angular/forms';
@NgModule({
declarations: [
...
],
imports: [
+ FormsModule,
],
...
コンポーネントのテストをするための準備
テンプレートに記述した <form>
には ngForm ディレクティブが適用されます。
これらのディレクティブは FormsModule で定義されているため、テスト環境でもモジュールの読み込みが必要です。
Angular v5.2.8 form_providers.ts
https://github.com/angular/angular/blob/5.2.8/packages/forms/src/form_providers.ts#L16-L24
https://github.com/angular/angular/blob/5.2.8/packages/forms/src/directives.ts#L68
// sample2.component.spec.ts
import {
async,
ComponentFixture,
+ fakeAsync,
TestBed,
+ tick
} from '@angular/core/testing';
+ import {
+ FormsModule,
+ NgForm
+ } from '@angular/forms';
+ import { By } from '@angular/platform-browser';
describe('Sample2Component', () => {
let component: Sample2Component;
let fixture: ComponentFixture<Sample2Component>;
beforeEach(async(() => {
TestBed.configureTestingModule({
+ imports: [FormsModule],
declarations: [ Sample2Component ]
})
.compileComponents();
}));
コンポーネントのプロパティがテンプレートに反映される事を検証
テンプレートに追加した <input>
を使って検証してみます。
// sample2.component.spec.ts
beforeEach(() => {
fixture = TestBed.createComponent(Sample2Component);
component = fixture.componentInstance;
- fixture.detectChanges(); // ①
});
...
+ it('input - コンポーネントのプロパティがフォームに反映される', fakeAsync(() => {
+ component.name = 'abc'; // ②
+ fixture.detectChanges(); // ③
+ tick();
+
+ const element = fixture.debugElement.query(By.css('[name=name]')).nativeElement as HTMLInputElement;
+ expect(element.value).toBe('abc');
+ }));
こちらも Sample1Component に書いたテストとほぼ同じですが、ひとつ異なるところがあります。
beforeEach で fixture.detectChanges()
を呼び出した後に(①)コンポーネントのプロパティを変更し(②)再び fixture.detectChanges()
を呼び出すと(③)、テンプレートが更新されませんでした。
このため fixture.detectChanges()
はテストスイート内だけで呼び出すように変更しています。
フォームを通して検証する
テンプレートで <form>
を利用すると Angular によって NgForm ディレクティブが適用されます。
テンプレート内では <form #myForm="ngForm">
のように記述して NgForm ディレクティブに紐付いたクラスのインスタンスを参照する事ができます。
テストコードで同じようにフォームを参照するには injector を使用します。
// sample2.component.spec.ts
+ function getForm() {
+ return fixture.debugElement.children[0].injector.get(NgForm);
+ }
...
it('input コンポーネントのプロパティがフォームに反映される', fakeAsync(() => {
component.name = 'abc';
fixture.detectChanges();
tick();
const element = fixture.debugElement.query(By.css('[name=name]')).nativeElement as HTMLInputElement;
expect(element.value).toBe('abc');
+ expect(getForm().value.name).toBe('abc');
}));
フォームの持つ値を検証するには value プロパティを利用します。
値以外にもバリデーション結果などフォーム特有のプロパティが参照できるため、テストのアプローチを増やす事ができますね。
フォームの値を変更してコンポーネントのプロパティが変わる事を検証
最初に fakeAsync / tick を利用して ngModel の非同期処理を終了させる必要があります。
// sample2.component.spec.ts
+ it('input フォームの値の変更がコンポーネントに反映される', fakeAsync(() => {
+ fixture.detectChanges();
+ tick();
+
+ const element = fixture.debugElement.query(By.css('[name=name]')).nativeElement as HTMLInputElement;
+ element.value = 'def';
+ element.dispatchEvent(new Event('input'));
+
+ expect(component.name).toBe('def');
+ expect(getForm().value.name).toBe('def');
+ }));
バリデーション
Template driven Form は HTML5 のバリデーションと同じ表記方法でフォームにバリデーションを持たせる事ができます。required 属性を指定してテストを書いてみましょう。
// sample2.component.html
<form>
...
- <input name="name" type="text" [(ngModel)]="name">
+ <input name="name" type="text" [(ngModel)]="name" required>
// sample2.component.spec.ts
+ it('input フォームのバリデーション', fakeAsync(() => {
+ fixture.detectChanges();
+ tick();
+
+ const element = fixture.debugElement.query(By.css('[name=name]')).nativeElement as HTMLInputElement;
+
+ element.value = '';
+ element.dispatchEvent(new Event('input'));
+ expect(getForm().valid).toBe(false);
+
+ element.value = 'abc';
+ element.dispatchEvent(new Event('input'));
+ expect(getForm().valid).toBe(true);
}));
実際の運用では HTML5 のバリデーションだけでは足りず、もっと複雑な動作が必要な場面が出てくるはずです。Angular はそのような場合 Template driven Form ではなく Reactive-forms の利用を推奨しています。