自動E2Eテスト導入時に大事にしたこと

今年の後半で保守開発をしているプロダクトに自動E2Eテストを導入しました。
現在のフェーズとしてはテストを実行する仕組みが整って日次実行が行われるようになり、いくつかの機能にテストを書いてみたというフェーズです。
まだテストケースの数は多くありませんが、CIでの日次実行が行われており、出荷前にデグレードを検出するなど手ごたえを感じています。

この記事では、この半年間の自動E2Eテストの導入時に何を大事にしながら取り組んだのかをまとめます。

# テストの目的を明確にすること
## 他のテスト機構とのすみ分け
### 自動E2Eテスト導入前の我々のプロダクトのテストアーキテクチャ

> - CIで実行されるJUnitTest（Javaプロセスのみ）の実行基盤
> - CIで実行されるJUnitTest（データベースにアクセスする）の実行基盤
> - 機能開発時に開発者が作成し協力会社の打鍵者の方に依頼するファンクショナルテスト
> - QAチームが作成しているブラックボックスE2Eシナリオテスト

参考

https://qiita.com/gengen0719/items/d42a7f60e9a0ebc6f345#%E3%81%93%E3%81%AE%E8%A8%98%E4%BA%8B%E3%81%AE%E5%86%85%E5%AE%B9

開発チームが実装できる自動E2Eテストの仕組みはなく、修正の確認や既存動作担保に多くの打鍵工数がかかっている状況でした。
状況としては今もまだ大きく変わっていませんが、打鍵者の方に依頼するファンクショナルテストを減らしていきたいと考えています。

### 自動E2Eテストの特徴

自動E2Eテストはサイズの大きなテストです。

https://speakerdeck.com/twada/automated-test-knowledge-from-savanna-202406-findy-dev-prod-con-edition?slide=36

そのため

- プロダクション相当のアプリケーション実行環境の準備が必要
- テストが壊れやすい
- テストが壊れたりFailした時に問題の箇所を特定するのが大変

というような特徴があり、特に保守コストが高くなるテストです。
プロダクトによってはあえて導入しないという戦略もあると思います。
テストの目的を明確にし、他のテスト機構とのすみ分けを考え、基本的には数を相対的に少なく保つ必要があります。

### 我々のプロダクトコードの特徴と自動E2Eテストの意義
我々が保守するプロダクトは古いプロダクトです。
Unitテストは目下頑張って整備をしていますが、整備されていないコードも多いです。

そういったコードに対して既存動作の担保をするためには外側から包むテストが重要です。
サーバーロジックがAPI化されている場合はAPIテストのコスパが良いと思いますが、それが難しい場合は自動E2Eテストを整備するメリットがあると思います。

今回の導入では不具合修正やUnitテストの整備などで既存のコードに手が入った際のデグレードの発生を抑えることを目的とし、基本動作に絞ってテストを整備していくことにしました。
これによってクラスやモジュールの結合部分の動作は自動E2Eテストで担保し、ロジックのパターン網羅はUnitテストで担保する形を目指していきます。

# 自動実行できる仕組みを作り日次で実行すること
個人的にはこれが一番重要なことであると思います。
対象のプロダクトコードに変更が行われているのに実行されないテストはすぐに壊れます。
壊れていることに気づかない、壊れたまま直さないといった状態だと、テストコードはすぐに腐ってしまいます。

- 毎日実行する仕組み
- 失敗したらすぐに通知をする仕組み
- すぐに修正することを促す仕組み

を作る必要があり、維持し続ける必要があります。

## コンテナでアプリケーションを起動できるようにする
毎日実行するCIに組み込む際の障害となるのが実行環境の準備です。
テスト専用のプロダクション相当のサーバーを準備するのも一つの方法ですが、無駄なコストがかかったり、更新処理を行った後にデータベースの状態を元に戻すのが大変です。
そういった点を考慮するとテスト対象のアプリケーションはコンテナで起動し、それに対してテストを行えるようにするべきでしょう。
プロダクション環境がコンテナで動作しているサービスであれば何の問題もないと思いますが、そうでない場合はここが最初のハードルになるのではないかと思います。
我々もがんばってコンテナで起動できるようにしました。

## テストに必要なデータの状態を用意する
すべてのテストが初期状態から実施できるアプリケーションである場合は問題ありませんが、たくさんの設定をする必要があったり、他システムから入ってきたデータをもとに動作するアプリケーションの場合はテストに必要なデータの状態を用意することも大変です。

我々のプロダクトは両方に該当するため、テスト用に起動したアプリケーションだけでは必要なデータを作成することができませんでした。
そのためあらかじめプロダクション相当のサーバーでテスト用のデータを作成し、それをボリュームにしてコンテナにアタッチする仕組みを作成しました。
これによってテスト実行時はいつも同じデータ状態でテストを実行することができるようになりました。

ただすべてのテストケースに対して一つのボリュームという構成になっているため、テストケースとデータの関連付けという点で課題があると考えており、今後テストケースが増えた際にも管理に無理がないかを検査しながら進めなければいけないと思っています。

# 自動E2Eテストの記述方法
テストの記述方法についてはスペシャリストの方に入っていただき、議論しながら検討したため、迷いなく進めることができました。

記述方法の詳細はこちら

https://qiita.com/gengen0719/items/9ac5d616fc8972ce1a4b

# テストコードを書いていく際の工夫
## 絶対に実行する操作の共通化
自動E2Eテストは実装のコストも大きなものとなります。
人間が行う場合は簡単な操作でも、要素を特定するロケーターを書くのが難しくて実装に時間がかかったりしますし、単純にすべてのオペレーションをコードに書き起こしていくのは大変です。
一方でどの機能でも行う操作を共通化しておくとかなり実装工数が削減されます。
そのため、そういった操作を共通化して実装しておくことは重要なことだと感じました。

我々のプロダクトでは以下のような処理を共通化して簡単に実装できるようにして進めました。
- ログイン
- どの機能でも同じ作りになっている設定
    - 機能の利用有無の設定
    - 機能ごとの認可設定
    - 画面への項目配置の設定
- 機能への画面遷移
など

共通化に際してはアプリケーションの作りや挙動を把握して取り組む必要があると感じました。

## 機能の特性に応じたテストコードの構造化

我々のプロダクトでは、大量の項目が配置できる入力画面、確認画面、完了画面というような構成になっている機能が多くあります。
（入力画面と確認画面では微妙にDOM要素の構造が違います）

入力画面で入力した内容が確認画面で正しく表示されるというようなテストを愚直に書くと

- 入力画面のロケーター
- 入力内容

という組み合わせと

- 確認画面のロケーター
- 入力内容

という組み合わせになってしまい、同じ項目を表現するコードが分散してしまいます。

これに対して例えば

- 入力画面のロケーター
- 入力内容
- 確認画面のロケーター

のような構造体を作り、入力操作の実装と確認画面でのアサーションの実装に渡すと綺麗にまとめられます。

特に多くの機能で同一の構成になっている画面に対しては実装を構造化してまとめる工夫が必要だということを感じました。
この際にもアプリケーションの作りや挙動を把握して取り組む必要があると感じました。

この辺りのテクニックはもっとテスト実装のパターンを多く経験した後に、具体的な事例として改めてまとめたいなと思っています。

# これからに向けた意気込み
色々実装における障害はあったのですが、終始楽しく実装に取り組むことができ、運用のスタートラインに立つことができました。
また冒頭に記載したように、あまりテストケースが多くない状況でも、自動E2Eテストがないと見落としていたであろうデグレードを発見するなど手ごたえも感じているので、来年はより実装を進めて打鍵工数の削減など成果を出していきたいと思っています。

おわり。