はじめに
Webアプリなどで申し込みフォームなどのページを作った際に、入力フォームのバリデーションを実装することが多々あると思います。
バリデーション付きフォームのテストは人の手で行うと入力作業に時間がかかったり、テストパターンの数が多くなったりするので、テストコードによる自動化が好ましいです。
今回はAngular&Jasmineを利用し、input要素に対するバリデーションのテストコード作成方法に関して説明します。
環境
以下の環境で実装しました。
- Windows 10
- Angular: 8.1.3
- Angular CLI: 8.1.3
- Node: 12.14.1
- typescript: 3.4.5
準備(Reactive Formの作成)
以下のコマンドでinput
コンポーネントを作成します。(コンポーネント名は任意)
$ ng g component input
次に、モジュールでReactiveFormModule
を有効化します。
...
import { ReactiveFormsModule } from '@angular/forms';
...
@NgModule({
declarations: [InputComponent],
imports: [
...
ReactiveFormsModule, // テンプレート内でFormControlなどを利用するため
...
]
...
})
export class AppModule{ }
そして、inputコンポーネントのテンプレートを作成します。今回は説明のため簡潔に。
<form [formGroup]="formGroup">
<div>
<input type="text" [formControl]="name">
</div>
</form>
次に、ReactiveFormを実装します。今回の記事ではrequired
(必須項目)とmaxLength
(最大文字数)のバリデーションを設定します。
import { Component, OnInit } from '@angular/core';
import { FormControl, FormBuilder, Validators } from '@angular/forms';
@Component({
selector: 'app-input',
templateUrl: './input.component.html',
styleUrls: ['./input.component.css']
})
export class InputComponent {
name = new FormControl('', [
Validators.required,
Validators.maxLength(10)
]);
formGroup = this.fb.group({
name: this.name
});
constructor(private fb: FormBuilder) { }
}
テスト項目の作成
今回実施するテスト項目は以下のようにします。
正常/異常 | 内容 | 期待結果 |
---|---|---|
正常 | 10文字の名前 | nameフォームが有効であること 入力した値を保持してること |
異常 | (入力なし) | FormGroupが無効であること |
異常 | (入力なし) | nameフォームが無効であること エラーの種類がrequiredであること |
異常 | 11文字の名前 | nameフォームが無効であること エラーの種類がmaxlengthであること |
異常系の1つ目のテストは、requiredエラーが出ているときにFormGroupは無効であることを確かめます。
テストコードの作成
さあ準備ができました。テストコードの作成に入りましょう。
コンポーネント作成時にAngular CLIによって自動で作成されたテストコード (input.component.spec.ts
) に追記していきます。
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ReactiveFormsModule, FormControl, FormGroup } from '@angular/forms';
import { InputComponent } from './input.component';
import { By } from '@angular/platform-browser';
describe('InputComponent', () => {
// テストごとに再代入するので変数はletで宣言
let component: InputComponent;
let fixture: ComponentFixture<InputComponent>;
let name: FormControl;
let formGroup: FormGroup;
let input: HTMLInputElement;
// beforeEachは各テストの前に実行される。
// ReactiveFormsModuleのimportsとInputComponentのdeclarationを設定されたテスト用のモジュールを準備
// async()であるのでモジュールのコンパイルが終了してから次のBeforeEachに移る
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ReactiveFormsModule],
declarations: [InputComponent]
})
.compileComponents();
}));
// Inputコンポーネントのインスタンス化を行う。
beforeEach(() => {
fixture = TestBed.createComponent(InputComponent);
component = fixture.componentInstance;
fixture.detectChanges();
// Reactiveフォームの状態を検証する際に利用する
formGroup = component.formGroup;
name = component.name;
// テンプレートから値を代入する際に利用するため、input要素を取得
input = fixture.debugElement.query(By.css('input')).nativeElement as HTMLInputElement;
});
describe('正常系', () => {
it('10文字の名前', () => {
// input要素に値を入れ、inputイベントを発火させる
input.value = 'Tony Stark';
input.dispatchEvent(new Event('input'));
// フォームに値が代入されるか、有効か確かめる
expect(name.value).toBe('Tony Stark');
expect(name.valid).toBeTruthy();
});
});
describe('異常系', () => {
// 異常系テスト(1)
it('何も入力していないとき(requiredエラーが発生する場合)にFormGroupは無効か', () => {
expect(formGroup.invalid).toBeTruthy();
});
// 異常系テスト(2)
it('必須項目が空である', () => {
const errors = name.errors;
// errorsは { required: true }
expect(name.invalid).toBeTruthy();
expect(errors.required).toBeTruthy();
});
// 異常系テスト(3)
it('11文字の名前', () => {
input.value = 'SteveRogers';
input.dispatchEvent(new Event('input'));
// errorsは { maxlength: { requiredLength: 10, actualLength: 11 } };
const errors = name.errors;
expect(name.invalid).toBeTruthy();
expect(errors.maxlength).toBeTruthy();
});
});
});
異常系テスト(1) - FormGroupの有効性
何も入力していないときにFormGroupが無効であることを確認します。無効であるのは、name
フォームが必須項目なのに入力されていないからです。
formGroup.invalid
プロパティはFormGroupの有効性を示しているので、Trueであることを確認します。
expect(formGroup.invalid).toBeTruthy();
異常系テスト(2) - 必須項目
input要素に何も入っていない(空文字)の場合に、無効であることとエラーの種類が required
に関するものであることを確かめます。
FormControlクラスはerrors
というプロパティを持ちます。errors
というバリデーションエラーに関するプロパティを持ちます。エラーが存在する場合は、ValidationErrors
型のオブジェクトを持ち、エラーがない場合はnull
となります。(参考:https://angular.io/api/forms/AbstractControl#errors)
よって、required
バリデーションを満たしていない場合は、errors
プロパティは以下のオブジェクトを持ちます。
{ required: true }
よって、name
FormControlがinvalidであること、errors
プロパティ自身がrequired
プロパティを保持しているか(=バリデーションエラーの種類がrequied
であること)を確認します。
expect(name.invalid).toBeTruthy();
expect(errors.required).toBeTruthy();
異常系テスト(3) - 最大文字数
最大文字数のバリデーションに関しても、required
の場合とほとんど変わりありません。
input要素に最大文字数以上の文字列が入力されている場合、FormControlのerrors
プロパティは以下のオブジェクトを持ちます。
{ maxlength: { requiredLength: 10, actualLength: 11 } }
よって、name
FormControlがinvalidであること、errors
プロパティ自身がmaxlength
プロパティを保持しているか(=バリデーションエラーの種類がmaxlength
であること)を確認します。
expect(name.invalid).toBeTruthy();
expect(errors.maxlength).toBeTruthy();
1点だけ注意です。Validatorのメソッド名はValidators.maxLength(10)
というようにmaxLength
なのですが、errorsのプロパティ名はmaxlength
で全部小文字です。
僕はタイプミスに気付かず、無駄に時間を費やしました。
カスタムバリデーションの場合
Reactive Formではカスタムバリデーションを実装することができますが、カスタムバリデーションのテストに関しても同様です。
カスタムバリデーションで定義したプロパティ名の存在有無で調べることができます。
// カスタムバリデーションで return { custom: true} としている場合
expect(errors.custom).toBeTruthy();
テスト実行
以下のコマンドでテストを実行します
$ ng test
テスト結果は以下の通りで、全項目成功しました。
13 01 2020 22:44:54.296:INFO [karma-server]: Karma v4.1.0 server started at http://0.0.0.0:9876/
13 01 2020 22:44:54.298:INFO [launcher]: Launching browsers Chrome with concurrency unlimited
13 01 2020 22:44:54.306:INFO [launcher]: Starting browser Chrome
13 01 2020 22:44:58.911:WARN [karma]: No captured browser, open http://localhost:9876/
13 01 2020 22:44:59.352:INFO [Chrome 79.0.3945 (Windows 10.0.0)]: Connected on socket Ey2iAJzKKTU09-8KAAAA with id 76527760
Chrome 79.0.3945 (Windows 10.0.0): Executed 4 of 4 SUCCESS (0.236 secs / 0.183 secs)
TOTAL: 4 SUCCESS
おわりに
今回はリアクティブフォームのバリデーションに関するテストに関して記事にしてみました。
Angularの公式サイトにはバリデーションに関するテストコードは載っていなかったので、色々調べながら実装しました。試行錯誤したものの、最終的にはプロパティの存在有無を調べるだけでした。
入力フォームのテストはテスト項目数が多くなりがち(境界値や禁則文字など)で、人の手によるテストは時間がかかってしまうので、テストコードを上手に活用していきたいと思います。
間違っている点などありましたら、遠慮なくご指摘ください。よろしくお願いします。