とんかつ時々あんどーなつ

〜たとえ低空でも飛行していられるように〜

Angular アプリケーションのユニットテストについて考えていること

はじめに

Angular アプリケーションとレイヤードアーキテクチャとディレクトリ構成 - とんかつ時々あんどーなつ に書いたようにアーキテクチャディレクトリ構成を考えるときに、そのメリットとしてテスタビリティの向上も期待の一つとすることが多い。プロダクションコードの責務を分離することで、どのコードにどういうテストを書けばよいか(逆にどこのテストを考えなくてよいか)が明確になると考えている。

この記事では、前の記事で示した構成の Angular アプリケーションに対してどのようなテストを書くとよいか検討したのでまとめてみることとする。なお、ここでいうテストコードはユニットテストを指している。

NOTE: アウトプットしてみるものの実践してはいないため、うまくまとまっていない部分もある

検討対象

今回は、前の記事で示した features/ の中に限定して検討する。

src/app/features/xxxx/
├── containers/
│   ├── container1/
│   │   ├── container1.component.html
│   │   ├── container1.component.scss
│   │   ├── container1.component.spec.ts
│   │   ├── container1.component.ts
│   │   ├── container1.service.spec.ts
│   │   ├── container1.service.ts
│   │   ├── container1.store.spec.ts
│   │   └── container1.store.ts
│   └── container2/
├── pages
│   └── page
│       ├── page.component.html
│       ├── page.component.spec.ts
│       └── page.component.ts
├── routes.ts
└── views
    ├── presentational1/
    │   ├── presentational1.component.html
    │   ├── presentational1.component.scss
    │   ├── presentational1.component.spec.ts
    │   └── presentational1.component.ts
    └── presentational2/

どういうテストコードを用意するか

page component

routed component としての役割をもつ page component は URL path と表示したい component が一致しているかをテストしたい。そのため RouterTestingHarness を使ったテストがマッチする。

describe('DashboardPageComponent', () => {
  let harness: RouterTestingHarness;
  let component: DashboardPageComponent;
  let router: Router;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [DashboardPageComponent],
      providers: [provideRouter(routes)],
    })
      .overrideComponent(DashboardPageComponent, { remove: { imports: [DashboardComponent] }, add: { imports: [MockDashboardComponent] } })
      .compileComponents();

    router = TestBed.inject(Router);
    harness = await RouterTestingHarness.create();
    component = await harness.navigateByUrl('', DashboardPageComponent);

    harness.detectChanges();
  });

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

provideRouter() の引数に渡す routes はアプリケーショントップレベルの Route[] にする。これにより / にアクセスしたときに DashboardPageComponent が表示されることをテストすることができる。

DashboardPageComponent には子コンポーネントとして DashboardComponent(container component) を表示しているが、テストではこれを mock に置き換えている。routed component は URL と表示するコンポーネントが一致することや URL パラメータにより振る舞いが変わる場合など、Router に関するテストに集中したいため mock を使い container component より先のことは考えないようにする。

もう少し詳細な内容は standalone な Angular の page component をどうテストするか - とんかつ時々あんどーなつ が参考になるかもしれない。

presentational component

presentational component は基本的に外部のものには依存せず、Input を受け取りそれを画面に表示することが責務となる。そのため Testing Library を使ったテストがマッチする。

Testing Library については Angular アプリケーションに Testing Library を導入する - とんかつ時々あんどーなつ にも書いている。

ロジックのテストというよりは、実際に component をレンダリングし、要素のチェックはイベントの発火などがメインのテストケースとなる。

container component / service / store

残るは container component / service / store となる。container と service は役割が異なるが、どちらも store と強く結びついているという点に注目したい。

すべてを独立させてテストすることもできるが、store と container / service と store の 2 パターンで考えることにした。 container は store の状態に応じたロジックを書くことが多い。service は infrastructure などからデータを取得し、それを store に保存するのが主な仕事になる。それぞれを分離させず、一連の流れとしてテストしたほうが、よりユースケースとしてテストできると感じている。

まとめ

ユニットテストを書くことはあたりまえになってきた一方で、どのようなテストを書くのがより効果的かを悩むことが多くなったので現時点での考えをアウトプットしてみた。 まだまだ検討の余地はあるので、引き続き深ぼっていきたい。