CircleCI上のフロントエンドテストの実行時間を30分から10分に短縮した話
はじめに
こんにちは!株式会社POLでエンジニアをやっている @show_kanamaru です!
POLは「研究者の可能性を最大化するプラットフォームを創造する」をビジョンに、理系学生に特化した採用サービス、および研究開発者・技術者に特化した転職/採用サービスの2サービスを運営しています。
POLではJestを使ってフロントエンドのテストを実行しているのですが、CircleCI上でのテストの実行時間がなんと30分。。。
これはいかんと思い、CI上でのテスト実行時間短縮に挑戦しました。
改善前
改善前のCIのワークフローはこちら
APIのユニットテストとフロントエンドのテストを並列に回しているのですが、フロントエンドのテストが30分以上かかっているのが大きな課題でした。
なぜここまで時間がかかっているか・・・
それは単純にテストの数が多く、それを直列に回しているからでした。
画像の通り、352個のTest Suitesと1654個のTestsと256個のSnapshotsがあります。(実行時間が遅いという課題はあるものの、LabBaseリリース当時はテストファイルが一切なかったことを考えると頑張ってきたなあ。。。)
では早速いろいろチャレンジしてみましょう!
解決策①:ワークフローの分解
LabBaseのフロントエンドのディレクトリ構成は以下のようになっています
- app
- student
- 学生ドメインのコードを管理
- client
- 企業ドメインコードを管理
- common
- 学生、企業共通のドメイン(新規登録、メッセージなど)を管理
- shared
- 上記のディレクトリ全てで使用するものの管理(マスターデータなど)
現状だとプロジェクト直下にjest.config.jsがあり、上記全てのディレクトリのテストをまとめて実行していました。
まずは、student, client, common, sharedのディレクトリごとにワークフローを分離し、それぞれを並列で実行するように変更しました。
.circleci/config.yml
# 変更前
frontend_test:
executor:
name: frontend_defaults
parameters:
persist_coverage:
type: boolean
default: false
steps:
- checkout
- judge_job_execution:
target_directory: project/app
- restore_frontend_setup
- setup_frontend
- run:
name: Flow type check
command: cd project/app && npm run flow
no_output_timeout: 10m
- run:
name: TypeScript type check
command: cd project/app && npm run tsc
no_output_timeout: 10m
- run:
name: ESLint and Jest check
command: cd project/app && ls && npm run lint && npm run test
no_output_timeout: 10m
- when:
condition: << parameters.persist_coverage >>
steps:
- store_artifacts:
path: project/app/coverage
- report_to_slack:
postname: "frontend coverage report"
username: "frontend-coverage-report"
output_path: "project/app/coverage/lcov-report/index.html"
# 変更後
student_frontend_test:
executor:
name: frontend_defaults
steps:
- checkout
- judge_job_execution:
target_directory: project/app/student
- restore_frontend_setup
- setup_frontend
- run:
name: Flow type check
command: cd project/app && npm run flow app/student
no_output_timeout: 10m
- run:
name: TypeScript type check
command: cd project/app && npm run tsc:student
no_output_timeout: 10m
- run:
name: ESLint and Jest check
command: cd project/app && ls && npm run lint app/student && npm run test:student
no_output_timeout: 10m
# client, common, sharedも同様
package.json
# 変更前
"test": "cross-env NODE_ENV=test NODE_ICU_DATA=node_modules/full-icu jest --coverage --logHeapUsage --runInBand --silent",
# 変更後
"test:student": "cross-env NODE_ENV=test NODE_ICU_DATA=node_modules/full-icu jest --config=./app/student/jest.student.config.js --runInBand --coverage --silent app/student/",
"test:client": "cross-env NODE_ENV=test NODE_ICU_DATA=node_modules/full-icu jest --config=./app/client/jest.client.config.js --runInBand --coverage --silent app/client/",
"test:common": "cross-env NODE_ENV=test NODE_ICU_DATA=node_modules/full-icu jest --config=./app/common/jest.common.config.js --runInBand --coverage --silent app/common/",
"test:shared": "cross-env NODE_ENV=test NODE_ICU_DATA=node_modules/full-icu jest --config=./app/shared/jest.shared.config.js --runInBand --coverage --silent app/shared/",
また、今まではアプリケーション全体でカバレッジを計測し、カバレッジは設定した値を下回ったらエラーを出すようにしていました。
jest.config.js
module.exports = {
coverageThreshold: {
global: {
statements: ◯,
branches: ◯,
functions: ◯,
lines: ◯,
},
},
};
上記を大元の設定ファイルとし、それぞれのディレクトリごとにjestの設定ファイルを作り、該当するディレクトリ配下のファイルだけでカバレッジを計測するように変更しました
app/student/jest.student.config.js
const baseConfig = require('../../jest.config');
module.exports = {
...baseConfig,
rootDir: '../../',
collectCoverageFrom: [
'app/student/*.{js,jsx,ts,tsx}',
'!app/student/*.test.{js,jsx,ts,tsx}',
],
coverageThreshold: {
global: {
statements: ◯,
branches: ◯,
functions: ◯,
lines: ◯,
},
},
};
TypeScriptの型チェックも同様に、それぞれのディレクトリにtsconfigファイルを作成し対応しました。
ここまでの対応でテストの実行時間はこんな感じ。
ワークフローを分解して並列で回した結果12分くらいになりました。
だいぶ改善されましたね!
解決策②:coverageオプションの削除
jestを実行する際に--coverageオプションをつけるとカバレッジ情報を出力してくれます。これでカバレッジの値がある一定値を下回ったらエラーを出すような運用をしていました。
ただ、このカバレッジオプションをつけるとテスト対象のファイルを全部見に行ってカバレッジを出してくれるので結構な時間がかかるんですよね。
よし、カバレッジオプションを消そう!
そもそもカバレッジの値がある一定値を下回ったらエラーを出していたのは、当時テストを書く文化がなかったためのもので、今は毎コミットごとにカバレッジを出す必要はないかなと思っています。
--coverageオプションを消してみると・・・
9分弱まで短縮されました!!!
番外編
--coverageオプションを消してしまったので、別の方法でカバレッジを定期的に計測したい。。とはいえ、時間がかかるのでCircleCI上では実行したくない。。。
ということで、こんな感じのシェルスクリプトを書いてみました。
#!/bin/sh
RESULT=$(yarn test:shared &)
wait
if [[ $RESULT =~ files[^shared]*shared ]]; then
REG_TEXT=`echo ${BASH_REMATCH[0]} | sed -E 's/ +//g' | sed -E 's/files\|+//g' | sed -E 's/\|shared+//g'`
LIST=(${REG_TEXT//|/ })
STATEMENTS=${LIST[0]}
BRANCHES=${LIST[1]}
FUNCTIONS=${LIST[2]}
LINES=${LIST[3]}
echo ${STATEMENTS}
echo ${BRANCHES}
echo ${FUNCTIONS}
echo ${LINES}
fi
yarn test:sharedを実行して得られた結果から欲しいカバレッジの情報(STATEMENTS、BRANCHES、FUNCTIONS、LINES)を取り出す簡単なものです。
これを週1くらいで実行してスプシに吐き出すのが良いかなと思っています。この辺はチームメンバーと相談して運用方法を決めていく予定。他社さんがどうやってカバレッジ計測してる気になりますね。。。
最後に
CI上でのフロントのテスト実行時間を約1/3に短縮できました!ただ、現状毎回全てのテストを実行しているものを差分だけ実行するようにしたり、テスト自体を並列化したりとまだまだできることはあるので、引き続きDX向上に励んでいきたいと思っています!
また、僕たちのチームではバリューストリームマッピングを実施し、「リードタイムの短縮」に挑戦しています!
まずはこんな感じで仕様策定〜リリースまでのリードタイムを可視化し、少しずつ改善していってます。
今回はフロントエンドテストの実行時間を短縮しましたが、上記の画像通りまだまだ課題だらけ。。。
DevOpsやアジャイル開発推進に興味がある方!ぜひ一緒に課題を解決していきましょう!!!
そして、エンジニア、デザイナー、プロダクトマネージャーも大募集してます!お話しだけでも構いませんのでお気軽にお声がけください!!!