ネットワークサービス部のid:uwenoです。
先日チームメンバーと一緒にScrum Fest Niigata 2023というイベントに参加してきました。
このイベントではテストや品質も大きなテーマの1つになっており、
他社での事例やベストプラクティスを持ち帰ることができ、とても有意義な時間でした。
あと新潟のお酒と寿司は美味しかったです
改めまして、今回からは「CI・CD戦略編」と題しまして、ソフトウェアの設計やその背後にある思想といった、 より大局的なテーマを扱っていきます。
第1回目となる今回は、自動テストの戦略についてご紹介させていただきます。
これまでも度々お伝えしてきた通り、私たちはCIを使って自動テストを行っています。 CIで自動テストを行うことで以下のようなメリットがあり、開発をより効率的に進めることができます。
- テストをパスしないコードがマージされることを防ぐ
- コードレビューの前にテストがパスしていないことに気づける
テストサイズ
Googleが提唱するテストの分類法である、「テストサイズ」を参考に、 テストの種類small、medium、largeの3種類に分類して実装しています。 テストサイズに関する説明は、偉大な先人たちがすでに大変わかりやすく説明してくださっている1ため、 ここでは詳細の説明は省きます。 代わりに、私たちが具体的にこの分類をどのように活用しているかを中心にご紹介します。 なお、small 、medium、largeのいずれのテストも、Pytestを使って実装しています。
smallテスト
Pythonのみで処理が完結し、外部への通信が必要ないテストはsmallテストとして分類、実装します。 DDDでいうところのドメイン層に対するコードには、必ずsmallテストを実装するというルールを設けており、 テストカバレッジ(命令網羅)が100%でなければテストが失敗するようにしています。 (アプリケーション構成の詳細については、今後別の記事で取り上げる予定です)
テストカバレッジの集計対象にはしていませんが、ドメイン層以外のテストをsmallテストに含めることもあります。 以下はsmallテストを実行するコマンドの例です。
$ poetry run pytest \ --cov=application.module.domain \ --cov-fail-under=100 \ tests/small/module
smallテストは通常、Pythonのコードに対するユニットテストであり、 テストの事前準備のためのコードは少なく、テストの行数も比較的短いです。
mediumテスト
Pythonのみで処理が完結せず、DBやMQなど外部のミドルウェアに接続する必要があるテストはmediumテストとして分類、実装します。 私たちが開発しているアプリケーションでは、DDDでいうところのユースケース層やインタフェース層で外部のミドルウェアに依存があり、 mediumテストに分類されるものが多いです。 ユースケース層やインタフェース層のテストであっても、ミドルウェアへのアクセスが不要な場合はsmallテストに分類します。
私たちのソフトウェアで実装している典型的なmediumテストの例として、Web APIのテストが挙げられます。 FastAPIにはTestClientを作成する機能があり、 これを用いてWeb APIを呼び出すテストを実装しています。 Web APIにリクエストをした結果と、DBの状態を照合して、アプリケーションが想定通りに動作しているか確認します。
開発者が手元の環境でmediumテストを実行するためには、事前にdockerなどを使ってDBとMQをローカルで立ち上げる必要があります。 CIでの実行時には、パイプライン上でDBとMQのコンテナを起動し、mediumテストのジョブがDBとMQを利用できるように設定しています。
mediumテストでは、ミドルウェアのセットアップやクリーンアップの手順をコードに含める必要があります。 例えば、データベーススキーマを作成し、必要なデータを挿入するようなコードが頻出します。 また、ドメイン層以下のロジックを組み合わせた結合テストとなることも多く、 smallテストと比べて複雑で行数の長いテストになりやすいです。
largeテスト
largeテストでは、本番環境とほぼ近しいテスト環境にデプロイしたシステムに対して、E2Eテストを実施しています。 本連載の初回でもご紹介させていただいた通り、 私たちのチームはプライベートブリッジというネットワークサービスのバックエンドを開発しています。 E2Eテストでは、実際のユーザーがシステムを利用する際のユースケースを想定してテストを行い、 アプリケーション、DB、ネットワーク機器などが期待された振る舞いをするかを確認します。
E2Eテストのより詳しい内容については、次回あらためてご紹介する予定です。
Largeテストの実行には本番環境に類似したテスト環境を用意する必要がありますので、 自動テストの準備、実行には大きなコストがかかります。
ここまでご紹介したテストの外観は以下の通りです。
$ tree tests -L 3 tests ├── small │ ├── module1 │ │ ├── domain │ │ └── usecase │ └── module2 │ ├── domain │ └── interface ├── medium │ ├── module1 │ │ ├── interface │ │ └── usecase │ └── module2 │ ├── interface │ └── usecase ├── large . . .
テスト実行の頻度とタイミング
smallテストとmediumテストは、CI上でも数分程度で実行可能であり、 またそれぞれのテストは独立しているため同時に複数のパイプラインで実行できます。 そのため、smallテストmediumテストは、MRのプッシュ時にその都度パイプラインで実行しています。
一方で、largeテストは実行にかかる時間が比較的長く、 またテスト環境を占有してしまうため複数のMRのテストを同時に実行できません。 そのため、largeテストは1日1回の夜間でのスケージュール実行と、任意のタイミングでのトリガー実行に限定しています。
全部largeテストでよくない? いや、ダメです
あたらしいテストケースを追加したい時、どのテストサイズで実装するのが適切でしょうか?
largeテストであればE2Eレベルで動作を確認できますし、mediumテスト以下でテストしているコードも実行できそうです。 単純に考えれば、すべてのテストケースをlargeテストとして実装することでより安心安全なシステムになりそうですね。
しかし、テストの実装に必要なコスト(テストコードの行数や複雑性)は一般に、small -> medium -> large の順で大きくなります。 また、テストの実行に必要なコスト(実行時間)も同様に、small -> medium -> large の順で大きくなります。
参考までに、私たちのソフトウェアで実装されているテストをまとめると以下の表のようになります。
テストサイズ | テストケースの数 | 実行時間(分) | テストコードの行数 | テストケース1つあたりの実行時間(秒) | テストケース1つあたりのテストコードの行数 |
---|---|---|---|---|---|
small | 3600 | 2.5 | 53000 | 0.042 | 15 |
medium | 1500 | 4.5 | 55000 | 0.18 | 37 |
large | 75 | 25 | 9000 | 20 | 120 |
テストケース1つあたりのテストコードの行数に注目すると、mediumテストの実装にはsmallテストの約2.4倍のコスト(行数)がかかっており、 largeテストの実装にはsmallテストの約8.0倍のコストがかかっています。 テストケース1つあたりの実行時間でみても、mediumテストはsmallテストの約4.3倍、 largeテストはsmallテストの約480倍のコスト(実行時間)がかかっています。
すべてのテストケースをlargeテストとして実装した場合、テストの実装・実行のコストが膨大になってしまうため、開発速度の低下に繋がります。
テストピラミッド
ユニットテスト、インテグレーションテスト、E2Eテストの割合を適切にすることで、 高い信頼性を保ちつつ開発を高速・効率化させる、テストピラミッドという考え方があります2。
この考え方によると、テストケースの数をユニットテスト < インタグレーションテスト < E2Eテストの順になるように実装します。 ちょうどピラミッドのような関係ですね。
テストサイズによる分類とユニットテスト、インテグレーションテスト、E2Eテストの分類は違うものなのですが、 私たちのソフトウェアでは設計上、これら2つの分類の境界はほぼ同じです。
そのため、smallテスト、mediumテスト、largeテストのテストケースの数が、ピラミッドのようにバランスよくなるよう意識しています。
私たちのソフトウェアで実装されているテストケースの割合は
となっています。
まとめ
今回は私たちが自動テストを実装する上で意識している戦略、考え方についてご紹介させていただきました。 まとめると、私たちの自動テストは以下のような構成を取ります。
最適なテスト戦略はそのソフトウェアごとに異なると思いますが、 今回ご紹介した私たちのテスト戦略が自動テストの導入・改善の参考になれば幸いです。
次回は今回内容を割愛したE2Eテストについて、改めて詳しくご紹介する予定です。
連載バックナンバー
CI戦術編
- 【CI戦術編 その1】alembic check コマンドを活用したマイグレーションスクリプト生成忘れ防止
- 【CI戦術編 その2】コードレビューを充実したものにする方法、あるいは一生残る恥ずかしい履歴を作らないように
- 【CI戦術編 その3】Pythonのimportのことならまかせろ isort
- 【CI戦術編 その4】Blackを利用したコーディングスタイルの統一
- 【CI戦術編 その5】Pythonで明示的に型を書く理由
- 【CI戦術編 その6】Python開発の強い味方 Pylint
- 【CI戦術編 その7】 pyupgradeを使って最新の記法に対応してみた
- 【CI戦術編 その8】OAS(OpenAPI Specification)で仕様書を自動生成しよう
- 【CI戦術編 その9】自動生成しか勝たん openapi-typescript
- 【CI戦術編 その10】 Renovateで依存ライブラリのアップデートに負けない方法
- 【CI戦術編 その11】Trivy: あなたの使ってるライブラリ、大丈夫ですか?
- 【CI戦術編 その12】CI戦術編総まとめ — いつ何を使うべきか
- CI・CD戦略編
- テストサイズの説明はt-wadaさんの記事が分かりやすくておすすめです。↩
- テストピラミッドの説明もこれまたt-wadaさんの記事が大変わかりやすいです。↩