はじめに
これは Angular Advent Calendar 2022 1 日目の記事です。
今回は Angular アプリケーションに Testing Library を導入してみたので紹介します。
Testing Library とは
Testing Library | Testing Library
ユーザーがページ上で要素を見つけるのと同じように DOM node を見つけ操作することを可能にするライブラリです。つまり、実際にユーザーがアプリケーションを使うようにテストをすることができるということです。
また、Testing Library 自体は Angular 専用ではなく多くのフレームワークで使用することができます。
導入方法はパッケージをインストールするだけなので、公式ページを参照することをオススメします。Angular Testing Library | Testing Library
検証用のアプリケーションを用意
今回は Tour of Heroes - download example の完成形となるコードをダウンロードして使用しています。
この記事で対象にしたコンポーネントのコードも示しておきます。
// src/app/dashboard/dashboard.component.ts import { Component, OnInit } from '@angular/core'; import { Hero } from '../hero'; import { HeroService } from '../hero.service'; @Component({ selector: 'app-dashboard', templateUrl: './dashboard.component.html', styleUrls: [ './dashboard.component.css' ] }) export class DashboardComponent implements OnInit { heroes: Hero[] = []; constructor(private heroService: HeroService) { } ngOnInit(): void { this.getHeroes(); } getHeroes(): void { this.heroService.getHeroes() .subscribe(heroes => this.heroes = heroes.slice(1, 5)); } }
<!-- src/app/dashboard/dashboard.component.html --> <h2>Top Heroes</h2> <div class="heroes-menu"> <a *ngFor="let hero of heroes" routerLink="/detail/{{hero.id}}"> {{hero.name}} </a> </div> <app-hero-search></app-hero-search>
既存テストの確認
// src/app/dashboard/dashboard.component.spec.ts import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { of } from 'rxjs'; import { HeroSearchComponent } from '../hero-search/hero-search.component'; import { HeroService } from '../hero.service'; import { HEROES } from '../mock-heroes'; import { DashboardComponent } from './dashboard.component'; describe('DashboardComponent', () => { let component: DashboardComponent; let fixture: ComponentFixture<DashboardComponent>; let heroService; let getHeroesSpy: jasmine.Spy; beforeEach(waitForAsync(() => { heroService = jasmine.createSpyObj('HeroService', ['getHeroes']); getHeroesSpy = heroService.getHeroes.and.returnValue(of(HEROES)); TestBed .configureTestingModule({ declarations: [DashboardComponent, HeroSearchComponent], imports: [RouterTestingModule.withRoutes([])], providers: [{provide: HeroService, useValue: heroService}] }) .compileComponents(); fixture = TestBed.createComponent(DashboardComponent); component = fixture.componentInstance; fixture.detectChanges(); })); it('should be created', () => { expect(component).toBeTruthy(); }); it('should display "Top Heroes" as headline', () => { expect(fixture.nativeElement.querySelector('h2').textContent).toEqual('Top Heroes'); }); it('should call heroService', waitForAsync(() => { expect(getHeroesSpy.calls.any()).toBe(true); })); it('should display 4 links', waitForAsync(() => { expect(fixture.nativeElement.querySelectorAll('a').length).toEqual(4); })); });
既存のコードでは以下 4 つのテストケースがあります。
Testing Library で書き換える
事前準備
テストをおこなうには、まず対象のコンポーネントをレンダリングする必要があり、Testing Library では render()
を使用します。
対象のコンポーネントが依存しているサービスや子コンポーネントがあるなら TestBed.configureTestingModule()
に渡すように render()
の第 2 引数としてオブジェクトで渡すことができます。
また render()
の返り値には DOM ノードが RenderResult として返り fixture
などを取り出すことができます。
let component: DashboardComponent; let fixture: ComponentFixture<DashboardComponent>; const heroService = jasmine.createSpyObj('HeroService', ['getHeroes']); const getHeroesSpy = heroService.getHeroes.and.returnValue(of(HEROES)); beforeEach(async () => { const renderResult = await render(DashboardComponent, { declarations: [HeroSearchComponent], providers: [{ provide: HeroService, useValue: heroService }], }); fixture = renderResult.fixture; component = fixture.componentInstance; }); });
Angular のテスト用 API である TestBed を使わず記述できるようになるのが特徴です。
テストケースの作成
コンポーネントが作成されていること
render()
の返り値から componentInstance を取り出せるため、このテストケースは特に変更なく、そのままとなります。
h2 タグで "Top Heroes" が表示されていること
もともとのテストケースを再掲します。
it('should display "Top Heroes" as headline', () => { expect(fixture.nativeElement.querySelector('h2').textContent).toEqual('Top Heroes'); });
Testing Library では role
を指定することで対象の要素を見つけることができます。
画面上から 'Top Heroes' と書かれた heading
role を探し、その存在を確認するテストケースが以下になります。
it('should display "Top Heroes" as headline', () => { expect(screen.getByRole('heading', { name: 'Top Heroes' })).toBeTruthy(); });
今回は getByRole()
を使っていますが、どのメソッドを使うのが適切かわからない場合は、まず getByText()
を使って対象のテキストがあるかを探します。
it('should display "Top Heroes" as headline', () => { expect(screen.getByText('Top Heroes')).toBeTruthy(); });
各メソッドの第 2 引数には MatcherOptions を渡すことができますが、その際に suggest
を指定することでよりよいメソッドが提案されます。
it('should display "Top Heroes" as headline', () => { expect(screen.getByText('Top Heroes', { suggest: true })).toBeTruthy(); });
これを使って積極的によりよいメソッドを使えると良さそうです。
HeroService::getHeroes() が呼ばれていること
これは Testing Library を使っても変わるところがないので割愛します。
a タグが 4 つあること
既存のテストケースは a タグが 4 つあることを確認しています。 このままテストしてもいいですが、どのような a タグなのかを示せたほうがテストケースとしてはより有用だと考えられます。
今回の場合は「Bombasto, Celeritas, Magneta, RubberMan と書かれたリンクがあること」をそれぞれ確認するテストケースに変更します。
it('should display 4 links', () => { expect(screen.getByRole('link', { name: /Bombasto/i })).toBeTruthy(); expect(screen.getByRole('link', { name: /Celeritas/i })).toBeTruthy(); expect(screen.getByRole('link', { name: /Magneta/i })).toBeTruthy(); expect(screen.getByRole('link', { name: /RubberMan/i })).toBeTruthy(); });
このようなテストケースも簡単にかけます。
まとめ
今回は導入ということで、簡単なコンポーネントについてテストを書いてみましたが、比較的直感的に書けることがわかりました。 後回しにしがちな view のテストも書いていきたい気持ちになったのではないでしょうか?
明日は nontangent さんです!