【エンジニアブログ】CI/CD自動テストの高速化を実施した話!

本記事は、当社オウンドメディア「Doors」に移転しました。

約5秒後に自動的にリダイレクトします。


自社開発プロダクト「Rtoaster」をはじめとする製品群を扱うXaaSユニットの社員によるエンジニアブログです。今回は、反比例になりがちな「テストの拡充」と「開発効率」の両立に向けた課題を解決するため、CI/CD自動テストの高速化を実施した事例をご紹介します!

はじめに

XaaSユニット プロダクトエンジニアリング所属の田中です。
ブレインパッドは、企業のデータ活用・DXやデジタルマーケティングを支援するデータビジネス・プラットフォーム「Rtoaster(アールトースター)」を開発・提供しています。
現在、「Rtoaster」では品質改善のため、テスト拡充に重点的に取り組んでいます。テストの拡充により、信頼性の高いプロダクトを開発することが可能となりますが、一方でテストケースの増加は開発効率や開発者体験に影響を及ぼします。今回この課題を解決するために、CI/CD自動テストの高速化を行いましたのでその事例を紹介します。

アプリケーションの構成とテスト環境

今回のCI/CD自動テスト高速化の対象は、フロントエンドで以下のような構成となっています。

フレームワーク:Nuxt2 (Vue2 , Composition-API)
言語:TypeScript
テストツール:jest, swc, vue-test-utils, vue-testing-library
CI:CircleCI

すでに高速化の取り組みとして、ts-jest(tsc) の代わりにswcの導入、Typescript4 から 5へのアップデートなどを行い、テストの速度を約20%向上しています。しかし日々増え続けるテストケースにより、CI/CDの実行が40分以上かかっている状況でした。

高速化の取り組み

ワークフロー、ジョブの最適化

まずワークフロー、ジョブの見直しを行いました。ビルドとテストが結合されたジョブとなっていたため分解し、さらにテスト内のlint, prettier, unittest, integration test も細かく分解しました。これにより後述するテストの分割・並列処理実行時にリソースを効果的に利用することができるようになります。
そして各ジョブの前段にnpm moduleをキャッシュするジョブを追加し、後続ジョブでキャッシュを利用するようにしました。

ジョブを最適化し、1日1回CIを定期実行することで品質面は担保しつつ、デプロイ時は自動テストをスキップ出来る構成とし時間を短縮しました。

変更前、変更後のワークフローは以下になります。

   before                         after

テスト分割・並列実行

次にテストの実行時間がボトルネックとなっていたため、テスト分割し並列実行を行いました。
分割方法はタイミングベースを利用することで、以前のテスト実行のタイミングデータを使って適切に均等となるように分割してくれます。

以下が設定例です。

unittest-frontend:
    executor: frontend-base
    parallelism: 4
    resource_class: large
         steps:
      - run:
          name: Test
          command: |
            TESTFILES=$(circleci tests glob "tests/**/*.test.ts" | circleci tests split --split-by=timings)
            npm run test -- $TESTFILES


この並列実行により、全体の実行時間を短縮することができました。



分割後のテストカバレッジの結合・生成

テスト分割・並列実行するデメリットとして、従来の方法では全体のカバレッジが取得できなくなってしまいます。これを解決するため、各ノードで生成されたカバレッジを結合し、全体のカバレッジが生成できるようにしました。

各ノードで実行されたカバレッジデータをpersist_to_workspace を使ってジョブ間で利用できるようにします。この時$CIRCLE_NODE_INDEX を利用しカバレッジデータの保存先がノード間で重複しないようにします。

 - run:
    name: Test
    command: |
      TESTFILES=$(circleci tests glob "tests/**/*.test.ts" | circleci tests split --split-by=timings)
      npm run test -- $TESTFILES
      mv target/coverage coverage_$CIRCLE_NODE_INDEX
- persist_to_workspace:
  root: /home/circleci/project
  paths:
  - coverage_*/

combine_coverage というジョブでは以下の処理を行ってます。

1.前ジョブで保存した各カバレッジデータを取得
2.複数のカバレッジデータを1つのJSONファイルにマージ
3.集約されたカバレッジデータを元にHTMLレポートを生成
4.生成されたレポートを保存

combine_coverage:
    executor: frontend-base
    resource_class: medium+
    steps:
      - attach_workspace:
          at: /home/circleci/project
      - run:
          name: Install module
          command:  npm install nyc istanbul-merge istanbul
      - run:
          name: Combine coverage report
          command: |
            cp -R hoge/coverage_* ./
            npx istanbul-merge --out coverage/coverage.json coverage_*/coverage-final.json
            npx istanbul report --include coverage/coverage.json --dir coverage html
      - store_artifacts:
          path: coverage



このようにして、並列に実行したテスト結果をCI上で一元的に確認できるようになりました。



まとめ

これらの取り組みの結果、CIの実行時間は42分から18分に、デプロイの時間も42分から7分に短縮し、それぞれ2.3倍、6倍の高速化を実現することができました。
今後もプロダクトの品質を維持しつつ、開発効率、開発者体験を向上する取り組みを続けていきたいと思います。


ブレインパッドでは新卒採用・中途採用共にまだまだ仲間を募集しています。
ご興味のある方は、是非採用サイトをご覧ください!

www.brainpad.co.jp
www.brainpad.co.jp