FJCT Tech blog

富士通クラウドテクノロジーズ公式エンジニアブログです

富士通クラウドテクノロジーズ

FJCT/Tech blog

【CI・CD戦略編 その2】自動化されたE2Eテストでプロダクトの品質を維持しよう

ネットワークサービス部の田上(id:rtagami)です。

スマートウォッチは興味ないと言っていた家族に諸般の事情でPixel Watchを買って差し上げたのですが、 健康管理機能がお気に入りのようで家の中でも就寝中でも、ずっと身につけているようです (もっと早く買って) 。 自分は出るとウワサのPixel Watch 2を待って既存のスマートウォッチから買い換える予定です。

この記事では、わたしたちのプロダクトのE2Eテストについてご紹介し、 それがわたしたちのプロダクトにどう役立っているかをご説明します。

なお、前回の記事は 【CI・CD戦略編 その1】CIによる自動テスト ~品質と速度の両立のために~ でした。

一般的にはウェブブラウザーなどを自動操作して挙動を確認するものをE2Eテストと呼ぶことが多いですが、 わたしたちのプロダクトではそのようなテストとは別に、 バックエンドAPIのみを対象としたE2Eテストを実施しています。 本記事では、このバックエンドAPIのみを対象としたE2Eテストについてご説明します。

わたしたちのプロダクトのE2Eテスト

わたしたちのプロダクトのE2Eテストの方針は、 プロダクション環境と同じ構成でデプロイされたシステムのバックエンドAPIに対してリクエストを送り、 ネットワーク機器に対して期待された変更が行われることを確認するというものです。

smallテスト・mediumテストと比較すると、次のような違いがあります。

  • プロダクション環境と同じ構成でアプリケーションがデプロイされる
    • 複数のマシンに分散されたコンテナ実行環境へデプロイされる
    • プロダクション環境と同じやり方で作られたコンテナイメージを使う
    • ワーカープロセスもプロダクション環境と同じようにデプロイする
  • モックのようなものを一切使わない
    • 本物のクラスタ化されたDBを使う
    • 本物のクラスタ化されたMQブローカーを使う
    • 本物と同じ挙動をするネットワーク機器を使う
    • 認証や通知等の外部サービスも本物を使う
  • テスト対象はバックエンドAPIの挙動
    • 非同期処理を(も)実行し、終了するまでポーリングをする
  • アサーション対象はバックエンドAPIのレスポンスとネットワーク機器の状態

わたしたちのプロダクトでのsmallテスト・mediumテスト・largeテストの実装の差を、 簡単に図示してみました(イメージ図のため、大幅に簡略化しています)。

なぜE2Eテストをしているのか

前回の記事でご紹介した通り、 わたしたちのプロダクトはテストをsmallテスト・mediumテスト・largeテストに分類しており、 E2Eテストはlargeテストに分類されます。 E2Eテストがテスト全体の中で占める割合は限定的ですが、 プロダクトの品質に問題がないことを最終確認するものとして重要な役割を果たしています。

テストしづらいコードをテストしたい

GoogleTest Sizesによれば、 ローカルホスト以外のネットワークアクセスと外部システムへのアクセスはlargeテストでしかできません。

ローカルホストで動かしやすいミドルウェアはmediumテストでも実物を使えますが、 外部のWeb APIやネットワーク機器のようなローカルホストで動かすことが困難なものは、 smallテスト・mediumテストではモックせざるを得ません。

これらはインタフェースが明確に決まっていることが多いのでそれらを扱うコードに手を入れる機会はあまり無いのですが、 何かしらの事情で手を入れなければならなくなった時に手動でしかテストできないというのは非現実的です。

テストしづらい処理の流れもテストしたい

わたしたちのプロダクトには、時間のかかる処理を非同期で実行する機能があります。 この非同期処理は、全般的な処理を行うWeb APIと、特定の処理を担うワーカープロセスとして実装しています。

Web APIのロジックもワーカープロセスのロジックもsmallテストやmediumテストで確認してはいますが、 非同期処理の最初から最後までを通して実行するテストがないと、ロジックに破綻が無いか不安が残ります。

頻繁にリリースしたい

詳細は別記事にてご説明させていただく予定ですが、わたしたちのプロダクトは、 変更内容が何であれ必ず週1回はmainブランチの内容をプロダクション環境にリリースしています。

しかし、リリースの頻度が上がることに対して品質面での懸念を持つ人がいることも事実です。 開発者として、自信を持ってリリースできる状態であると宣言する根拠が必要です。

積極的にライブラリや言語処理系をアップデートしたい

以前の記事 (【CI戦術編 その10】【CI戦術編 その11】) でご紹介した通り、わたしたちはRenovateやTrivyなどを通して 積極的にライブラリや言語処理系をアップデートしていく方針を取っています (さばききれずにRenovateのレートリミットに当たる事もしばしばですが…)。

ビッグバンリリースが辛いのと同じ理由でライブラリのアップデートを後回しにしてもメリットは一切ありませんが、 ライブラリや言語処理系のアップデートでプロダクトが壊れてしまっては本末転倒です。

わたしたちのE2Eテストでしていること

以上のような課題を解決するために、わたしたちはE2Eテストを書いています。 もう少し具体的に、わたしたちが書いているE2Eテストをご説明します。

お客様に利用されるユースケースが確実に動くことを確認する

わたしたちのプロダクトには、 ニフクラのコントロールパネルを通してお客様が直接利用するユースケースと、 それ以外のユースケースがあります。

(それ以外のユースケースには、 弊社の運用者が専用の管理画面を通して使うものや、 監視のためのものなどいろいろなものがあります。)

お客様に利用されるユースケースが何かしらの問題で正しく動作しない状態は、 いわゆる"障害"といわれる状態ですので、 最大限の努力をもって回避する必要があります。

お客様に利用されるユースケースにはネットワーク機器の状態を変更するものが多く含まれています。 ネットワーク機器にアクセスするコードは実際のネットワーク機器の振る舞いとの相互作用があり、 本物と同じ挙動をするネットワーク機器にアクセスさせることでコードが想定どおり機能するか確認する必要があります。

また、ネットワーク機器へのアクセスはすべて非同期処理として実装しています。 これらの非同期処理は複数種類のワーカープロセスが起動していることに依存しており、 (厳密には不可能ではないのですが)mediumテストの枠内には組み入れづらいものです。

非同期処理が正しく動くことや、ネットワーク機器の状態変更が正しく行われることを主な観点として、 お客様に利用されるユースケースが間違いなく動くことを確認するためにわたしたちはE2Eテストを書いています。

リグレッションがないことを確認する

前述の通り、わたしたちはコードに対する変更はできるだけ小さく頻繁に行い、 リリースもできる限り頻繁に行うことにしています。

コミットを大きくしたりリリース頻度を下げれば問題が発現する機会を減少する効果はあるものの、 問題が含まれる("障害"が発生する)可能性を減らすことはできないからです。

むしろ、一度問題が発現すると、どこに問題があったかの特定が困難になることでしょう。

変更やリリースに対する懸念を払拭した状態で自信をもって頻繁にリリースするために、 わたしたちはE2Eテストを実行しています。

E2Eテストはmainブランチに対して毎日深夜に実行しており、 これによってその時点でのmainブランチが壊れていないことを確認しています。 また、E2Eテストを任意のマージリクエストに対して実行できるようにもなっています。 RenovateやTrivyを元にした変更や、非同期処理に影響がある可能性のある変更に対しては、 コードレビューの際にE2Eテストを実行してそれが成功することを確認しています。

E2Eテストの問題点

プロダクトの品質を守るという観点でメリットの多いE2Eテストですが、 一方でデメリットも多くあり、 あらゆる状況に利用できるテスト手法ではありません。

遅い

わたしたちのE2Eテストには非同期処理の動作確認が多く含まれており、 非同期処理を含むユースケースのテストには相応の時間がかかります。 一般論として、単一の処理にかかる時間が長い場合には非同期処理を採用することが多いので、 そのような処理はテストにも時間がかかるのは当たり前ともいえます。

実行に時間のかかるテストは、テスト自体を書くのにも時間がかかりますので、 前回の記事でも触れたテストピラミッドのような形を目指して、 戦略的にE2Eテストのカバー範囲を狭める事が必要です。 t_wadaさんの資料 もご参照ください。 わたしたちのプロダクトのE2Eテストは、 お客様に利用されるユースケースが確実に動くこと、 という観点に絞って実装しています。

コードが煩雑

E2Eテストは実際のクライアントを模倣することによってバックエンドAPIの挙動を確認しています。 ネットワーク越しにリソースを操作するコードは、 一般論としてコードが煩雑になりがちです。 ネットワーク的な問題に遭遇する可能性やステータスコードが異常を示す場合などを考慮した上で、 必要であればリトライなども行う必要があるからです。

また、非同期処理をテストする際には非同期処理が完了するまで待ってからアサーションを行う必要がありますが、 非同期処理の完了を待つためには状態をポーリングする実装が必要になったりもします。 この部分もコードが煩雑になりがちです。

そして、わたしたちのプロダクトの場合はE2Eテストのアサーション対象がネットワーク機器の状態です。 ネットワーク機器の状態が所望の状態であることを確認する必要がありますが、 これもまた、一筋縄ではいかないコードになる事が多いです。

環境の準備が大変

テスト対象はプロダクション環境と同じ構成でデプロイするということは既に記載した通りですが、 これをやろうとすると当然ながらプロダクション環境と同じものをもう一セット用意する必要があります。

それだけでもなかなかの手間ですが、E2Eテスト環境はプロダクション環境と違って 事前に定められた既知の状態のデータを持っている必要があるという独自の要求もあります。

たとえば、わたしたちのプロダクトのE2Eテストでは、 テストの開始前に不要なゴミデータを削除した上で、 既知の初期データを投入したデータベースを用意する必要があります。 これを実現するためにわたしたちは「初期データ生成くん」という、 データベースを初期化した上で、 YAMLファイルに記載された情報を元に実際に使えるダミーデータを生成するツールを書いてメンテナンスしています。

E2Eテスト自体だけでなくE2Eテストを自動化するための環境や周辺ツールの整備などもあわせて行わないと、 E2Eテストを定期的に、あるいは気軽に実行する土壌が整わず、 苦労して書いたE2Eテストが活用されづらくなってしまいます。

さいごに

以上のようにデメリットも多くあるE2Eテストですが、 品質を維持しながらプロダクトの変更を続けるためには必須と言える仕掛けのひとつと言えます。 E2Eテストを適切に実装することによって、 チームが自信をもって活動できるようになるのと共に、 お客様に安心してお使いいただけるサービスを提供し続けられるのです。

次回は、お客様に安心してお使い頂けるサービスを提供するための別の仕掛けをご紹介致しますので、お楽しみに。

連載バックナンバー