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

# はじめに
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`コンポーネントを作成します。（コンポーネント名は任意）

```bash
$ ng g component input 
```

次に、モジュールで`ReactiveFormModule`を有効化します。

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

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

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

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

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

```typescript: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フォームが有効であること<br>入力した値を保持してること
異常 | （入力なし） | FormGroupが無効であること
異常| （入力なし） | nameフォームが無効であること<br>エラーの種類がrequiredであること
異常 | 11文字の名前| nameフォームが無効であること<br>エラーの種類がmaxlengthであること 

異常系の1つ目のテストは、requiredエラーが出ているときにFormGroupは無効であることを確かめます。

# テストコードの作成
さあ準備ができました。テストコードの作成に入りましょう。
コンポーネント作成時にAngular CLIによって自動で作成されたテストコード (`input.component.spec.ts`) に追記していきます。


```typescript: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であることを確認します。

```typescript
expect(formGroup.invalid).toBeTruthy();
```

## 異常系テスト(2) - 必須項目
input要素に何も入っていない（空文字）の場合に、無効であることとエラーの種類が `required`に関するものであることを確かめます。
FormControlクラスは`errors`というプロパティを持ちます。`errors`というバリデーションエラーに関するプロパティを持ちます。エラーが存在する場合は、`ValidationErrors`型のオブジェクトを持ち、エラーがない場合は`null`となります。（参考：https://angular.io/api/forms/AbstractControl#errors）
よって、`required`バリデーションを満たしていない場合は、`errors`プロパティは以下のオブジェクトを持ちます。

```typescript
{ required: true }
```

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

```typescript
expect(name.invalid).toBeTruthy();
expect(errors.required).toBeTruthy();
```
## 異常系テスト(3) - 最大文字数
最大文字数のバリデーションに関しても、`required`の場合とほとんど変わりありません。
input要素に最大文字数以上の文字列が入力されている場合、FormControlの`errors`プロパティは以下のオブジェクトを持ちます。

```typescript

{ maxlength: { requiredLength: 10, actualLength: 11 } }
```
よって、`name`FormControlがinvalidであること、`errors`プロパティ自身が`maxlength`プロパティを保持しているか（＝バリデーションエラーの種類が`maxlength`であること）を確認します。

```typescript

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

```
1点だけ注意です。Validatorのメソッド名は`Validators.maxLength(10)`というように`maxLength`なのですが、errorsのプロパティ名は`maxlength`で全部小文字です。
僕はタイプミスに気付かず、無駄に時間を費やしました。

## カスタムバリデーションの場合
Reactive Formではカスタムバリデーションを実装することができますが、カスタムバリデーションのテストに関しても同様です。
カスタムバリデーションで定義したプロパティ名の存在有無で調べることができます。

```typescript
// カスタムバリデーションで return { custom: true} としている場合
expect(errors.custom).toBeTruthy();
```

# テスト実行
以下のコマンドでテストを実行します

```bash
$ ng test
```

テスト結果は以下の通りで、全項目成功しました。

```bash
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/356779/4c8c395e-07db-d43f-faf2-f912a1b5384c.png)


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

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

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

# 参考
- [Angularのフォームのイントロダクション](https://angular.jp/guide/forms-overview)
- [リアクティブフォーム](https://angular.jp/guide/reactive-forms)
- [Angular コンポーネントのテスト（sample3 / Reactive form を利用したコンポーネントのテスト）](https://qiita.com/ringtail003/items/06de6e759d0b5768313f)