[Angular] コンポーネントのテストを簡単に書きたい - Testing Library と Spectator

# はじめに

Angularのコンポーネントテストは、同じようなコードを何度も書いたり、非同期回りがよくわからなかったり、少々つらみがあります。

ここでは、以下の2つのライブラリの書き心地を比較してみたいと思います。

* [Testing Library](https://testing-library.com/docs/angular-testing-library/intro)
* [Spectator](https://github.com/ngneat/spectator) (Akitaの作者が作っているそうです）


# 題材

題材として、Angularチュートリアルの[Tour of Heroes](https://angular.io/tutorial)を使います。


# インストール方法

npmを使うだけです。

## Testing Library

```
npm install @testing-library/angular --save-dev
```

## Spectator

```
npm install @ngneat/spectator --save-dev
```

# コンポーネントインスタンス（テスト対象）の生成

## オリジナル

オリジナルです。不要なものもある気もしますが長いですね。

```ts
  let component: DashboardComponent;
  let fixture: ComponentFixture<DashboardComponent>;
  let heroService;
  let getHeroesSpy;

  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();
    })
  );

  beforeEach(() => {
    fixture = TestBed.createComponent(DashboardComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });
```

## Testing Library

`render`関数を呼ぶことで、表示結果込みのコンポーネントを生成できます。

```ts
  let component: RenderResult<DashboardComponent, DashboardComponent>;
  let heroService;
  let getHeroesSpy;

  beforeEach(async () => {
    heroService = jasmine.createSpyObj('HeroService', ['getHeroes']);
    getHeroesSpy = heroService.getHeroes.and.returnValue(of(HEROES));
    component = await render(DashboardComponent, {
      providers: [{ provide: HeroService, useValue: heroService }],
    });
  });
```


## Spectator

最初にファクトリを作ってから、コンポーネントを生成します。

```ts
  let component: Spectator<DashboardComponent>;
  const createComponent = createComponentFactory(DashboardComponent);
  let heroService;
  let getHeroesSpy;

  beforeEach(() => {
    heroService = jasmine.createSpyObj('HeroService', ['getHeroes']);
    getHeroesSpy = heroService.getHeroes.and.returnValue(of(HEROES));
    component = createComponent({
      providers: [{ provide: HeroService, useValue: heroService }],
    });
  });
```


# コンポーネントが作成されたかテスト

このあたりの書き方は変わるところないですね。

```ts
it('should be created', () => {
  expect(component).toBeTruthy();
});
```

Testing Library / Spectorでテストしているのはコンポーネントなのか？という疑問はあります。


# ヘッドラインに'Top Heroes'と出力されているかテスト

## オリジナル

```ts
it('should display "Top Heroes" as headline', () => {
  expect(fixture.nativeElement.querySelector('h2').textContent).toEqual(
    'Top Heroes'
  );
});
```

## Testing Library

h2の中のテキストを取得します。取得できなければ例外が発生するので、expectしなくてよいです。

```ts
it('should display "Top Heroes" as headline', () => {
  component.getByText('Top Heroes', { selector: 'h2' });
});
```


## Spectator

オリジナルより短く書けます。

```ts
it('should display "Top Heroes" as headline', () => {
  expect(component.query('h2').textContent).toEqual('Top Heroes');
});
```

# HeroServiceが呼ばれているかテスト

jasmineスパイのテストですので、書き方は変わらないですね。

```ts
it('should call heroService', waitForAsync(() => {
    expect(getHeroesSpy.calls.any()).toBe(true);
  })
);
```

# リンクが4つ出力されているかテスト

`a`タグの数を数えています。

## オリジナル

```ts
it('should display 4 links', waitForAsync(() => {
    expect(fixture.nativeElement.querySelectorAll('a').length).toEqual(4);
  })
);
```


## Testing Library

`queryAllByText`で全マッチして数を数えています。

```ts
it('should display 4 links', () => {
  expect(component.queryAllByText(/./, { selector: 'a' }).length).toEqual(4);
});
```

## Spectator

オリジナルより短く書けます。

```ts
it('should display 4 links', () => {
  expect(component.queryAll('a').length).toEqual(4);
});
```


# まとめと感想

スター数とダウンロード数は2021年4月12日現在の値です。

|  | Testing Library | Spectator |
|:-:|:--|:--|
| コンポーネント（テスト対象）の生成  | `TestBed`を用意せず、すっぱり書ける  | `TestBed`を用意せず、すっぱり書ける  |
| エレメントの取得  | get...By、query...Byは、ちょっと癖があるかもしれない  | ほぼほぼオリジナルのテストと同じ  |
| GitHubスター数 | 381[^1] | 1,413 |
| 週間ダウンロード数 | 17,820 | 41,504 |


[^1]: 使用している[@testing-library/dom](https://github.com/testing-library/dom-testing-library)のスター数は2,530です。

Angular以外も使っていたり、これから使う可能性がある場合は、ReactやVue等でも使えるTesting Libraryがよさそうです。

Angularのテストに慣れていたり、既存のテストを移行する場合は、Spectatorがわかりやすくてよさそうです。

作成したファイルは https://github.com/sengokyu/ex.angular-testing に置きました。



# リンク

* [Testing Library](https://testing-library.com/docs/angular-testing-library/intro)
    * [Good testing practices with 🦔 Angular Testing Library](https://timdeschryver.dev/blog/good-testing-practices-with-angular-testing-library)
* [Spectator](https://github.com/ngneat/spectator)
    * [Angular Unit Testing with Spectator? Great tool not only for easiest test cases!](https://medium.com/@kosmikos/angular-unit-testing-with-spectator-great-tool-not-only-for-easiest-test-cases-9de9977ff12d)
* [NPMトレンド](https://www.npmtrends.com/@ngneat/spectator-vs-@testing-library/angular)