開発覚書はてな版

個人的な開発関連の備忘録

【Angular】Componentテスト(クラス・DOM)

概要

Angular のComponentテストではクラスをテストする場合とDOMをテストする場合で、TestBed.configureTestingModule の設定方法が変わってきます。
設定方法や使い分けについて記載します。

angular.jp

実行環境

  • Node.js - 10.9.x

使用ライブラリ

  • Angular - 7.2.x

クラス・DOMテストの使い分け

クラステストについて

  • Input, Output, メソッド単位でテストを実装する場合
  • カバレッジを意識したテストを実装する場合
  • DOMテストより TestBed.configureTestingModule の設定が簡略化。imports 箇所が削減できます。

DOMテストについて

  • レンダリング後のDOMの状態をテストをしたい場合
  • データバインディングが出来ているかテストをしたい場合
  • 子Componentも踏まえたテストをしたい場合

使い分け方

  • 各プロジェクト・チームごとに異なりますが、カバレッジを消化するためのユニットテストの範囲であればクラスのテストで問題ありません。
  • クラスのテストの方がDOM構築を行わないためテストが速く終わります。CIなどで実行する場合、クラスのテストの方が良い場合があります。
  • DOMテストの場合、E2Eやクラステストとのテスト範囲をチーム内で決めておく必要があります。

サンプルソース

テスト対象

app.component.ts
import { Component, OnInit } from '@angular/core';

import { AppService } from './app.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  title = 'ng-sample';
  name = '';
  text = '';

  constructor(private appService: AppService) {}

  ngOnInit(): void {
    this.text = 'aaa';
  }

  onClick(): void {
    this.appService.getData().subscribe(data => {
      this.name = data.name;
    });
  }
}
app.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

export interface JsonData {
  id: number;
  name: string;
  age: number;
}

@Injectable({
  providedIn: 'root',
})
export class AppService {
  readonly URL = './assets/data.json';

  constructor(private http: HttpClient) { }

  getData(): Observable<JsonData> {
    return this.http.get<JsonData>(this.URL);
  }
}

クラステスト

import { TestBed, fakeAsync, tick } from '@angular/core/testing';
import { of } from 'rxjs';

import { AppComponent } from './app.component';
import { AppService } from './app.service';

describe('AppComponent - Class Test', () => {
  let component: AppComponent;
  let appService: AppService;

  beforeEach(() => {
    // providers に AppComponent を設定
    TestBed.configureTestingModule({
      providers: [
        AppComponent,
        { provide: AppService, useValue: {} }
      ]
    });

    component = TestBed.get(AppComponent);
    appService = TestBed.get(AppService);
  });

  it('should create', () => {
    expect(component).toBeDefined();
  });

  it(`should have as title 'ng-sample'`, () => {
    expect(component.title).toEqual('ng-sample');
  });

  it('ngOnInit', () => {
    // exercise
    component.ngOnInit();

    // verify
    expect(component.text).toBe('aaa');
  });

  it('onClick', fakeAsync(() => {
    // setup
    appService.getData = jasmine.createSpy().and.returnValue(of({ name: 'hoge' }));

    // exercise
    component.onClick();
    tick();

    // verify
    expect(component.name).toBe('hoge');
  }));
});

DOMテスト

import { TestBed, async, ComponentFixture, fakeAsync, tick } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { of } from 'rxjs';

import { AppComponent } from './app.component';
import { AppService } from './app.service';

describe('AppComponent - DOM Test', () => {
  let component: AppComponent;
  let fixture: ComponentFixture<AppComponent>;
  let appService: AppService;

  beforeEach(async(() => {
    // compileComponents 後、ComponentFixture 経由で AppComponent を取得
    TestBed.configureTestingModule({
      declarations: [
        AppComponent
      ],
      imports: [
        FormsModule
      ],
      providers: [
        { provide: AppService, useValue: {} }
      ]
    }).compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(AppComponent);
    component = fixture.componentInstance;
    appService = TestBed.get(AppService);
  });

  it('should create', () => {
    expect(component).toBeDefined();
  });

  it(`should have as title 'ng-sample'`, () => {
    expect(component.title).toEqual('ng-sample');
  });

  it('should render title in a h1 tag', () => {
    fixture.detectChanges();
    const compiled = fixture.debugElement.nativeElement;
    expect(compiled.querySelector('h1').textContent).toContain('Welcome to ng-sample!');
  });

  it('ngOnInit', () => {
    // exercise
    component.ngOnInit();

    // verify
    expect(component.text).toBe('aaa');
  });

  it('onClick', fakeAsync(() => {
    // setup
    appService.getData = jasmine.createSpy().and.returnValue(of({ name: 'hoge' }));

    // exercise
    component.onClick();
    tick();

    // verify
    expect(component.name).toBe('hoge');
  }));
});

サンプルソース一式

github.com

個人的な使い分け

クラステスト

  • Componentのロジック周りのテスト
  • カバレッジ消化
  • CIで利用前提

DOMテスト

  • Componentの表示周りのテスト
  • データバインディングやPipeで変換された表示内容を確認

終わりに

  • クラスのユニットテストを意識することで、Componentのコードがきれいになることが多いのでテストは意識した方が良い。
  • クラス・DOMとテスト速度に差が出るため、適用範囲についてチーム内のルール決めが必須。
  • テストコードの維持コストは e2e > DOM > クラス になるため、テスト観点が重要になる。