フロントエンドテスト自動化の試行錯誤について共有します！

# この記事を書く目的、背景
筆者は新卒3年目(このPJ参画時は2年目)のエンジニアになります。福岡のSIerでSEとして働いております。今回、社内アプリの開発PJのPLを任されフロントエンドテスト自動化でテストの戦略策定から実行の部分までで試行錯誤したので、フロントエンドテストをどう進めていくか迷ってる方などに共有できればと思います。

# 前提の整理
まずは前提条件を整理します。
## PJ概要

本PJでは以下のような技術スタックで開発を進めていました。

:::note info
技術スタック
フロントエンド:Angular
バックエンド:Node.js＋Express(TypeScriptで記述)
:::

また、ウォーターフォール型の開発で、規模の小さいアプリだったため製造は私ほぼ1人で行いました。単体テストフェーズから新卒1年目の後輩2人がメンバーとしてアサインされた形になります。私が製造したソースコードに対して、メンバーがテストコードを書く形になりました。

## フロントエンドアーキテクチャ
次にフロントエンドのアーキテクチャについて整理します。様々なフロントエンドのアーキテクチャを勉強した結果、「コンテナ・プレゼンテーションパターン」を採用しました。詳細については以下の記事で説明しているので見てみてください。

https://qiita.com/Kyle1106/items/325272e93b247cd45b43

簡単に説明するとビジネスロジックやルーティングなど見た目以外の部分はコンテナに、見た目の部分はプレゼンテーションに書こうねというアーキテクチャになります。


# テスティングフレームワークの選定
フロントエンドはAngularを採用しておりましたので、テストを書く際には候補として、どれかがほとんどだと思います。

:::note info
テスティングフレームワークの候補
* Jest
* Jasmine/Karma
* Mocha
:::
私は今回Jestを選択しました。理由としては以下です。
1\. キャッチアップコストが少なくてすむ
→バックエンドもTypeScriptで記述しているのでフロントエンド、バックエンド同じテストフレームワークが使えます。また、JestはAngularだけでなく、JavaScript、TypeScriptで最も人気なテスティングライブラリなので、ネットに落ちている情報が多いです。また、これからTyepeScriptを触っていく身として一番王道のツールを触っておきたいという個人的な思いもありました。

2\. 実行速度が速い
→Jestは実行速度が速いです。今回はCICDの仕組みは残念ながら導入できてませんが、将来的に導入する可能性が高いのでその際にテストの実行速度は重要項目です。

3\. 様々な便利機能がついている
→Jestには様々な便利機能がついてます。詳細については割愛しますがsnapshotやカバレッジの測定など様々なことができます。（今思うと全然使いこなせませんでしたが。。）

これらの理由からまずはテスティングフレークワークとして、Jestを選定しました。


また上述しましたが、今回はコンテナ・プレゼンテーションパターンを採用しているので、コンテナのテストについてはJestでテストできます。ただ、プレゼンテーション(UI)のテストを行うとなるとJestだけでは難しい部分も多く、AngualrTestingLibraryを採用しました。

https://testing-library.com/docs/angular-testing-library/intro/

こちらはTestingLibraryというものがReactやVueなどフレームワークごとに存在しており、UIのテストに特化したものになります。

# 単体テストの観点
次にどんなテスト観点でテストを行ったのかを説明します。
コンテナとプレゼンテーションで異なるので、分けて説明します。

## コンテナ層
コンテナでは主に、ルーティング、サービスクラスの読み出しなどを担当していますので主に以下のような観点でテストを行って追いました。
* コンポーネント初期化処理が正しく走るか
* サービスを正しく呼び出せるか
* ルーティング処理が呼び出されるか

などです。基本的には一つのロジックに対してそのロジックが正しく呼ばれているかをテストしていった形になります。


## プレゼンテーション層
プレゼンテーションでは主にUIを担当してるので以下のようなテスト観点でテストしていきました。
* 初期表示が正しいか
* ボタンやフォームなどが意図した通りに動作するか
* コンテナ層から受け取るデータ（モックデータ）を表示できるか

などです。

# 見えてきた課題
上のような観点での単体テストをJest+AngularTestingLibraryを用いて、３週間程度行いました。テストの実装は私ではなく、メンバーが行いました。その中でたくさんの課題が見えてきました。

## テスト実施にめちゃめちゃ時間がかかる
単体テストは以下のような進め方で行っていました。

1\.テスト仕様書の作成
まずはメンバーに仕様書を作成してもらいます。
プロダクションコードの実装は私、テストコードの実装はメンバー担当だったので、まずメンバーはこの仕様書の作成をするためにソースコードの読み込みや仕様の理解が必要です。1機能のテスト仕様書の作成に1~2時間程度かかることもありました。

2\.仕様書のレビュー
テストケースの抜け漏れがないかを私の方でレビューします

3\.テスト実装
レビューが終わり次第、テストを実装してもらいます。メンバーがまだ新卒1年目だったこともあり、ここにとても時間がかかりました。1機能あたり、テストコードの実装に5~Maxで14時間ほどかかっていました。

4\.コードレビュー
テストの抜け漏れがないか確認、テストの書き方が正しいか確認するため、またメンバーが新卒1年目ということもあり、教育のためにもコードレビューを行いました。実際に書いてもらったソースコードを私がコードレビューしていました。地味にこのレビューにとても時間がかかりました。

本来はプロダクションコードを実装した人が、テストコードも同じタイミングで実装することが理想的だと思います。ただ、今回は製造実施者とテスト実施者が別だったので、まずは自分の担当範囲の仕様を理解して、未経験のAngularのソースコードを読み込んで、それからテスト観点を考えて、初めて触るJestとTestingLibraryでテストを実装するということがまだ1年目のメンバーには少々難しかったようです。その中でもよく自分たちで調査してやってくれましたが、工数のことを考えるとこの策は得策ではないと考え始めました。


## 単体テストの品質面の問題
[単体テストの観点](#単体テストの観点)
今回は上述したような観点で単体テストを書いていました。工数の関係もあり、結合テストではフロントエンドのコンポーネント間の結合テストは行わずに、APIとの結合テストを実施する予定でした。ただ、フロントエンドテストについて勉強するうちにTestingーLibraryの作者のテスティングトロフィーという考え方を知りました。

https://kentcdodds.com/blog/the-testing-trophy-and-testing-classifications

フロントエンドで最も効率よくバグを検出する方法はコンポーネント間の結合テストを行うこという考え方です。その点今の単体テストでは、
* (コンテナから連携されるデータをモックデータにして)プレゼンテーションはそのデータを正しく表示できるか
* （プレゼンテーションから送信されるデータをモックデータにして）コンテナはそのデータを処理できるか、サービスに渡せるか

という観点でテストを行っており、実際にコンテナとプレゼンテーション、サービスを結合してテストをしているわけではないので、テスト品質が担保されているかと言われると不安になってきました。

このような問題点があるので、テストの進め方について一度立ち止まってこれらの問題点を解決するための以下の施策を考えました。

# ユースケース(画面の振る舞い)に基づいた単体・結合テストの実施
上記２つの問題点を解決するために、ユースケースに基づいたテストを実装することにしました。
## ユースケースに基づいたテストとは
上述したように今回私が設計したテストは、１機能をコンテナとプレゼンテーションに分けて詳細な内部ロジックをテストするホワイトボックステストになります。
   ↓

:::note info
現行の単体テストの観点

コンテナ層
* コンポーネント初期化処理が正しく走るか
* サービスを正しく呼び出せるか
* ルーティング処理が呼び出されるか

プレゼンテーション層
* 初期表示が正しいか
* ボタンやフォームなどが意図した通りに動作するか
* コンテナ層とデータ連携できるか
:::


ただ、このようなテストを実施するとなると、詳細なビジネスロジックの読み込みが必要なので、テスト仕様書の作成に時間がかかります。（工数に余裕があればこのようなテストケースを実施することも大事ですが）よりユースケースに近い挙動をテストすることで効率的にバグを取得することができます。IDとPWでログインできる、ログイン画面を例にユースケースに基づいたテストの具体的なテストシナリオの例を挙げます。

:::note info
ユースケースに基づいたテスト観点

*  ID入力欄とPW入力欄に有効な値を入れた時にログイン後の画面に遷移するか
*  ID入力欄とPW入力欄に考えられる様々な入力値を入れて、状況に応じたエラーメッセージが表示されるか
*  ID入力欄とPW入力欄に有効な値を入れた時にログインボタンが活性化するか
:::


などです。　プレゼンテーションのテストでテストしていた単体レベルのテストを実施しつつ、実際に値を入力したりボタンをクリックした際にその画面がどのように振る舞うのかをテストします。このような、よりユースケースに近い「振る舞い」をテストすれば、入力フォームの振る舞い（単体レベル）、入力値によるエラーメッセージの表示（結合レベル）、入力値とイベントによる画面遷移（結合レベル）といったように、テストの品質的に不安だったコンテナとプレゼンテーション、サービスの連携もテストできます。（というよりもはや振る舞いに着目しているので、コンテナ、プレゼンテーションなどといった概念がない）

### テストコードの堅牢性の向上
また、「振る舞い」をテストすることは、運用面でも役立ちます。具体的にはテストコードの堅牢性（壊れにくさ）が変わります。どういうことかというと、サービスを運用する中で機能追加などが入り、プロダクションソースを修正する、もしくはリファクタリングするといった場面があると思います。その時に詳細な内部ロジックをテストすると、少しプロダクションコードを修正しただけで、テストコードが壊れて、テストコードも修正しないといけなくなります。そうすると、工数が倍かかり、リファクタする気も起きません。ただ、「振る舞い」をテストすれば、画面としての振る舞いが変わらなければテストは成功します。このメリットは今回の問題と直接関係はしませんが、良かった点です。

## 振る舞いのテストを実現するためのCypressの導入


Cypressというのは、実際にブラウザを制御して操作する、主にはE2Eテストを行うためのテストフレームワークになります。

https://www.cypress.io/

私が今回実装したい振る舞いに着目したテストを実装するにはCypressが一番だと思い、今回導入を決めました。詳細な書き方については公式を参考にしていただきたいのですが、簡単なログイン画面のテストサンプルコードを記載します。
```typescript:
describe('Login Test', () => {
  it('有効なユーザ名とPWでログインできる', () => {
    // ログイン画面にアクセス
    cy.visit('/login');
    
    // ユーザー名とパスワードを入力してログインボタンをクリック
    cy.get('input[name="username"]').type('your_username');
    cy.get('input[name="password"]').type('your_password');
    cy.get('button[type="submit"]').click();

    // ログイン後のページにリダイレクトされたことを確認
    cy.url().should('include', '/dashboard');

  });

  it('不正なユーザ名を入力するとログインできない', () => {
    // ログイン画面にアクセス
    cy.visit('/login');

    // 不正なユーザー名とパスワードを入力してログインボタンをクリック
    cy.get('input[name="username"]').type('invalid_username');
    cy.get('input[name="password"]').type('invalid_password');
    cy.get('button[type="submit"]').click();

    // エラーメッセージが表示されることを確認
    cy.contains('Invalid username or password').should('exist');
  });
});
```

CypressではAPIからのレスポンスデータを定義することができるので、ログイン画面を例に挙げると、ログインボタンクリック時にAPIモックデータを返すことができます。そうすれば、Cypressでのテスト実行時にはブラウザが制御され勝手に値がフォームに入力され、ログインボタンをクリックし、ログイン成功時にはログイン後のページに遷移することができます。JestではNode上でテストが実行されていましたが、Cypressではこれらの操作をブラウザ上で画面制御して行うので、実際に画面を確認できてイメージがつきやすいです。実際の挙動を見た方がわかりやすいので公式動画を置いておきます。

https://www.cypress.io/



# テスト観点の修正とCypressの導入によるテスト効率の改善結果
課題がテスト観点の修正とCypressの導入によりどうなったかを説明します。

[テスト実施にめちゃめちゃ時間がかかる](#テスト実施にめちゃめちゃ時間がかかる)


1.テスト仕様書の作成
振る舞いを理解すればいい（仕様を理解さえすれば、詳細な内部ロジックは読まなくていい）ので仕様書作成の時間が30分~45分で終わるようになった。__（70~80%の時間削減）__

2.仕様書のレビュー
これは前と同じ。

3.テスト実装
Cypressが直感的で書きやすいのと、コンテナとプレゼンテーションごとにテストを書いていたのが画面につき１つで済むようになったので、30分~Max4時間で実装することができ、大幅な時間削減 __（70~80%の時間削減）__ を行うことができた。

4.コードレビュー
仕様書のレビューで「テスト観点が網羅されているか」のレビューは終わっており、「正しく実施されているか」は、Cypressでは直感的で簡単なので、最初だけコードレビューを実施して途中からは実装が不安なところだけレビューを実施することで大幅な時間削減。

[単体テストの品質面の問題](#単体テストの品質面の問題)

画面の振る舞いをテストすれば、もちろんコンテナとプレゼンテーション間の連携もテストしたことになっている（連携した結果、画面がどのように振る舞っているのかをテストしたので）こちらの問題も解決しました。

# フロントエンドテストの振り返り
今回はテストツールの選定からフロントエンド（APIも）の各工程のテストでどのようなことをテストし品質を担保するのかのテストの全体設計、各テスト工程ではどんな観点でテストを行うのかの観点設計、実施といった全ての工程を担当させていただきました。良かったことと改善できたことについて振り返りたいと思います。

## 良かったこと
### テストの全体像が理解できたこと
今までは仕様書を渡されてそれを実施するだけだったのでそこまで全体像を意識してきませんでしたが、テストの全体設計を経験できたことでテストの全体像を理解することができました。今回は初めてで勉強しながらだったのでうまくいかない箇所も多々ありましたが次回以降テスト設計を行うときは比較的スムーズに進められるのではと思ってます。

### 常にどうすればテストが進めやすくなるか、工数を削減できるかを考え続け途中で軌道修正できたこと
毎日テストの進捗を確認して、テストメンバーの声を聞き、どうやったら効率的にテストができるか工数を減らせるかを考えたことは本当にいい経験になりました。正直最初は「なんとかなるっしょ」と思ってたものが全然進捗が上がらない時は焦りました。ただ、テストの進捗が上がらないボトルネックを探して、それを解決するにはどうすればいいのか、さまざまな記事を読み漁り、休日に自分で試してみて、といった取り組みを続けたことで最終的にはテスト効率を上げることができました。何事もですが、常に今の状況をよくしようというスタンスでいるが大事だと改めて思いました。


## 改善点
### 綺麗なテストをしようとしまっていたこと。今の自分の状況に合わせたテスト戦略が立てられていなかったこと
綺麗なテストというのは要するに教科書通りのテストということです。ネットを調べると「単体テストでは〇〇な観点で〇〇をテストしましょう」。「結合テスト、総合テストでは〇〇なことをテストしましょう」といった情報がたくさん出てきます。もちろんそれらは正解で、各テストごとにテストするべき項目はおおよそ決まっています。ただ、単体テストや結合テストという言葉は会社やPJによっても定義がバラバラで例えば今回のようなフロントエンドの結合間テストのことを結合テストと呼ぶのも正解ですし、フロントエンドとAPIを連携させたテストも結合テストと呼ぶことも正解です。最初は勉強中ということもあり、「単体テストでは〇〇をテストしないといけないんだ」などと思い込んでいました。それゆえ、Cypressを導入する時も、「CypressはE2Eテストで使用するツールだから単体、結合テストで使っちゃいけないんだ」などといった間違った思い込みを発動してなかなか踏み切ることができませんでした。

本来一番理想的な形は

1.製造実施者とテスト実施者が同じで製造のソースのプルリクを出すタイミングでコンポーネントの単体テストもJestとTestingLibraryで実施する。

2.Cypressを用いて画面の振る舞いをテストする

といった流れが最も綺麗で品質的にもいいと思います。ただ、今回の私の状況下ではチームメンバーのスキルレベルや開発体制などを考慮した結果、今回の私の取り組みが一番良かったのではないかなと思っています。もちろん経験不足なので、仕方のない部分もありますが、なんでも脳死で、ネットに書いてある通りにやるのではなく、今の自分の状況にあった戦略を取ることが大事だと身をもって学びました。


# 学びとこれからのやりたいこと
今回、小さなPJではあるもののPLを任されて、テストツールの選定から、テスト設計、実行という一連の流れを経験できたことはとても勉強になりました。今までは簡単なテストを仕様書の通り書くだけでしたが、どんな技術を選択するか、そしてなぜその技術を選択するのか。また、どんな観点でテストを設計するのか、なぜその観点でいいのか、抜けている項目はどの段階のテストで確認するのか、といったテストの全体像を考えてから、各フェーズのテストで品質を担保できるように設計することは勉強になりました。

まだまだ浅い知識ですが、フロントエンドのテスト自動化を一人でやり切った、テストがうまくいかない原因を自分なりに考えて調査して、結果的にテスト工数の削減を実現できたことは自信になりましたし、色々な試作の結果、メンバーから「作業効率が上がってテスト効率がどんどん上がっていった」という声が聞けて嬉しく思いました。

まだまだ、storybookを使用したり、スナップショットテストを実行してレイアウトの確認まで自動化したり、CICDを取り入れたりと改善の余地がたくさんあるのでいいプロダクト作りを心がけていきたいです。フロントエンドのテスト自動化を初めて行いましたが私のように初めてフロントエンドのテスト自動化をやる方に届けば嬉しいです！読んでいただきありがとうございました！


# テスト設計で参考になったサイト

https://testing-library.com/docs/angular-testing-library/intro/

https://zenn.dev/lacolaco/books/angular-after-tutorial/viewer/testing-component

https://qiita.com/propellerpanda/items/4f09e08b1473a5e506c6

https://docs.cypress.io/guides/component-testing/angular/overview

https://www.alpha.co.jp/blog/202204_01/