LoginSignup
9
6

More than 3 years have passed since last update.

[Angular+Jasmine] Reactive Formでのinput要素に対するバリデーションテスト

Last updated at Posted at 2020-01-18

はじめに

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を有効化します。

app.modules.ts
...
import { ReactiveFormsModule } from '@angular/forms';
...

@NgModule({
  declarations: [InputComponent],
  imports: [
    ...
    ReactiveFormsModule, // テンプレート内でFormControlなどを利用するため
    ...
  ]
  ...
})
export class AppModule{ }

そして、inputコンポーネントのテンプレートを作成します。今回は説明のため簡潔に。

input.component.html
<form [formGroup]="formGroup">
    <div>
        <input type="text" [formControl]="name">
    </div>
</form>

次に、ReactiveFormを実装します。今回の記事ではrequired(必須項目)とmaxLength(最大文字数)のバリデーションを設定します。

input.component.ts

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) に追記していきます。

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 }

よって、nameFormControlがinvalidであること、errorsプロパティ自身がrequiredプロパティを保持しているか(=バリデーションエラーの種類がrequiedであること)を確認します。

expect(name.invalid).toBeTruthy();
expect(errors.required).toBeTruthy();

異常系テスト(3) - 最大文字数

最大文字数のバリデーションに関しても、requiredの場合とほとんど変わりありません。
input要素に最大文字数以上の文字列が入力されている場合、FormControlのerrorsプロパティは以下のオブジェクトを持ちます。


{ maxlength: { requiredLength: 10, actualLength: 11 } }

よって、nameFormControlが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

image.png

おわりに

今回はリアクティブフォームのバリデーションに関するテストに関して記事にしてみました。
Angularの公式サイトにはバリデーションに関するテストコードは載っていなかったので、色々調べながら実装しました。試行錯誤したものの、最終的にはプロパティの存在有無を調べるだけでした。

入力フォームのテストはテスト項目数が多くなりがち(境界値や禁則文字など)で、人の手によるテストは時間がかかってしまうので、テストコードを上手に活用していきたいと思います。

間違っている点などありましたら、遠慮なくご指摘ください。よろしくお願いします。

参考

9
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
6