テスト工程を見直してCI時間を半分(くらい)にした話
株式会社ココナラ 事業開発グループ 開発チームのかもと申します。
昨年10周年を迎えたココナラは、積み重ねた日々を体現するような巨大なプロダクトに成長しています。
一部はBFFやマイクロサービスのようなモダンなアーキテクチャに置き換えられていますが、まだまだ全てを分離するには至っていません。
巨大なプロダクトには大量のテストコードがつきものです。
そんなコードに手を入れ、GitHubにpushすると、レビューをお願いする前に何が起こるでしょうか?
そう、CI待ちです。私が勝手に呼んでいるだけなので、一般的な名称は知りません。
今回は、テスト工程を見直し、最小の労力で(ココ重要)CI時間を改善した話をお届けします。
発生していた問題
コードをGitHubにpushすると、CircleCI上でlintやテストが自動的に実行されます。
当時、この処理が完了するまでに15分程度かかっていました。待っている間は基本的にはコーヒータイムです。
ちょっと早まってレビュー依頼を出してしまうと、テストで失敗しているんですが……なんてことがままあり、エンジニアの生産性と忍耐力が削られることに。
これは、どげんかせんといかんですね。
改善
原因は主に以下の3つです。
- 巨大なテストの直列処理
- Flakyなテスト
- CI上での無駄なジョブ
一つ一つ見ていきましょう。
巨大なテストの直列処理
テストはCircleCIの分割と、各コンテナ内でのparallel_testsにより二重に並列化しています。
特にCircleCIは大変に頭が良く、テストファイルごとの実行時間をを解析した上で、コンテナごとにテストファイルを割り振ってくれます(設定により変更可能)。
いい感じに配分してくれる図
配分したファイルをparallel_testsでさらに並列化する図
各コンテナのログを見ていくと、ひときわ時間のかかっている妙なプロセスが見つかりました。
それがこちら。
「並列というのかい。なんとも贅沢な名だね。今からおまえの名前は直列だよ」
原因は画像の下部に見えるように、ファイル内に1800以上のテストケースが書かれていたことにあります。
前述のように、CircleCIはテストファイルごとの実行時間に応じてテストを分割します。これだけ巨大なテストファイルですと、当然かなりの時間がかかります。
そのため、1つのコンテナに1ファイルという贅沢な使い方をしてしまったわけです。
これの解消法は簡単です。
テストファイルを事前に分けておきましょう。できれば数秒、長くても2-3分で終わるようなテストファイルだけにしておけば、このような悲劇は避けられます。
当該テストは、数百件あるマスタデータをループし、1ループごとに複数のテストを実施するといったものでした。
ループ自体は避けられなかったため、テスト単位で別ファイルに分けることで、1ファイルあたりのテスト件数を減らす対策をしました。
Flakyなテスト
Flaky Testとは、実行結果が不安定なテストのことです。特にコードをいじっていないのに、成功したり失敗したりします。
何もいじっていないのに3回目で成功
原因はいくつかありましたが、特に多かったのはハードコーディングされたIDの衝突と、時間に関わるテストの2点でした。
IDの衝突はそのまま、テストケース内でハードコーディングした結果、Factoryで自動生成されるレコードとIDが衝突しDuplicateしてしまうパターンです。
ローカルでは問題なくても、CI上では前述のように並列実行しているため、たまたま同居したテストファイルと相性が悪いと発生します。
時間に関わるテストは、境界値テストなどで発生します。
有効期限が5分のトークンをテストしたいとしましょう。実行中に他のプロセスですこーしだけ負荷がかかり、ミリ秒単位でこの5分を超過してしまうと、テストが失敗します。
さて解消法ですが、これはもう失敗したテストを見直していくしかありません。
幸いなことにCircleCIにはFlaky Testを検出する機能が備わっています。
ありがたく使わせていただき、地道に潰していきましょう。
とはいえ、一説には7つテストがあれば1つはFlakyなどと言われているように、これはもはや終わりなき戦いです。
割り切りつつ、チーム内でテストを書くときの注意事項を共有するのが良いかもしれません。
CI上での無駄なジョブ
最後にCIスクリプトを見直します。CI/CDと称されるように、CIとCDはセットで語られることが多いです。
従来のスクリプトでは、テスト後にデプロイ用のビルドが実行されていました。これがまたちょっと時間がかかります。
ちょっと待って、(デプロイしない)featureブランチなのにデプロイ用のビルドいる?
いりませんね。というわけでちょちょいと書き換えてしまいましょう。
CircleCIのワークフローのドキュメントに則り、ブランチ名で該当のジョブをスキップさせれば解決です。
workflows:
version: 2
build_and_test:
jobs:
- build
filters:
branches:
ignore: /^(feature).*/ #featureブランチならビルドしない
- test
実際はもう少し複雑ですが、大体こんな感じです。
ただし、ブランチ名称のルール化は必須です。
結果
以上3つの施策を行ったところ、どうなったでしょうか。
だいたい8分程度になりました。50%オフです。お花摘みタイムくらいには短くできましたね。
最初に申し上げたように、今回は最小の対応となっています。
まだまだできることは多いはずです。次の担当者はきっとうまくやるでしょう。
最後に
ココナラでは、既存環境の改善だけではなく、新規機能・新規事業の開発も盛んに取り組んでいます。
他の記事もご覧いただき、興味を持たれた方は是非ともカジュアル面談にお越しください。
求人要項はこちら
Discussion