CircleCI 2.1 で ESLint と Jest を動かすテスト自動化の紹介

この記事はZennにも投稿しています。Zennでの閲覧を希望される方は、[こちら](https://zenn.dev/n3xus/articles/fad064d554944a)からご覧ください。

## はじめに
◆この記事は何か
この記事では、CircleCI 2.1 を使用して、ESLint と Jest を自動化する方法を紹介します。主にコード品質を向上させるための導入の取り組みや工夫した点について解説します。

git hooksを利用したmonorepo構成でのcommit時自動ESLintの導入などに関しては、別の記事で解説しています。
https://qiita.com/n3xus/items/0c5b9762daa3d3bf99d1

今回とは関係ないですが、自動保存時のESLint+Prettierの自動整形もおすすめです。

Githubの構成としては、1つのリポジトリにbackendディレクトリとfrontendディレクトリがあるような構成になります。

ESLint：JavaScriptやTypeScriptのコードを静的に解析し、構文エラーやコードスタイルの問題を検出するためのツール
Jest：JavaScriptやTypeScriptで書かれたコードをテストするためのフレームワーク

◆対象は誰か
CircleCIを利用したCI/CDパイプラインの導入を検討しているエンジニアや、それ以外にもコード品質向上に関心のある開発者を対象としています。

◆ねらいは何か
手動で行っていたテストやLint作業をCircleCIを使って自動化し、効率化と品質向上を図る方法の共有。


## 先に結論
CircleCIを活用することで、テストプロセスの自動化が容易になり、開発効率が大幅に向上しました。特に、PRへのエラーコメント自動送信や並列実行の導入が効果的でした。

## 概要
コード品質を維持し向上させるために、ESLintやJestを使用していますが、手動でのテスト実行では抜け漏れの発生するリスクがありました。そこで、CircleCIを導入してテストプロセスを自動化し、品質の担保と開発プロセスの効率化を実現しました。

## 工夫点

### 1. PRへのエラーコメント自動送信
GitHubのPRとCircleCIの管理画面を行き来する手間を減らすために、エラーが発生した際にはその詳細がPR上に自動でコメントされるように設定しました。これにより、開発者はCircleCIの管理画面にログインする手間が省け、迅速にエラー修正に取りかかることができます。


### 2. 開発環境とCircleCI環境の整合性
開発環境とCircleCIの環境を一致させることで、環境依存のエラーを防止しました。具体的には、DockerやNode.jsのバージョン、その他設定をCircleCIのコンテナに反映させました。

### 3. 並列実行の導入
フロントエンドのLint、バックエンドのLint、バックエンドのJestテストを独立して並列実行できるように設定しました。これにより、CI/CDパイプライン全体のスピードが向上しました。

### 4. キャッシュの有効活用
restore_cacheやsave_cacheを活用し、依存関係の再インストールを回避しました。このキャッシュ戦略により、ビルド時間がさらに短縮されました。

## 導入手順
### セットアップ
導入したいGithubリポジトリにadmin権限で参加しているGithubアカウントでCircleCIにログインすると、対象のProjectでSet Up Projectを押すことで連携できます。

CircleCIで期待した通りの動作をさせるには`.circleci/config.yml`を記載する必要があるため、まずは可能であれば動作確認用のリポジトリを作ってそこでテストするのがおすすめです。

フォークしたリポジトリでもCircleCIを動作させる場合の設定は下記に載っています。
https://circleci.com/docs/ja/oss/#build-pull-requests-from-forked-repositories

環境変数の設定もCircleCI管理画面から可能です。
https://circleci.com/docs/ja/set-environment-variable/#set-an-environment-variable-in-a-project

Githubアカウントでトークンを発行し、環境変数に設定することでCircleCI動作時にPR上へコメント送信することが可能になります。
https://github.com/settings/tokens

CircleCI管理画面のセットアップや設定完了後は、Githubリポジトリのアカウントをadmin以外の権限に戻してもCircleCIは動作可能です。

### `config.yml`の作成

CircleCIを動作させるために`.circleci/config.yml`を作成します。
このファイルを整えていき、それぞれの環境に合わせて期待した動作をさせるのが主な作業になります。

**完成形の例**
（実際よりかなり省略してます）

```yaml
version: 2.1

executors:
  node-executor:
    docker:
      - image: cimg/node:22.7.0

jobs:
  lint_frontend:
    executor: node-executor
    steps:
      - checkout
      - restore_cache:
          keys:
            - yarn-packages-{{ checksum "frontend/yarn.lock" }}

      - run:
          name: Install frontend dependencies
          command: |
            cd frontend
            yarn install

      - save_cache:
          key: yarn-packages-{{ checksum "frontend/yarn.lock" }}
          paths:
            - ~/.cache/yarn

      - run:
          name: Lint frontend
          command: |
            cd frontend
            yarn lint | tee lint-frontend-result.log || echo "export TEST_RESULT=$?" >> $BASH_ENV

      - run:
          name: Post result to Github
          command: |
            cd frontend
            if [ -f lint-frontend-result.log ] && [ $TEST_RESULT -ne 0 ]; then
              COMMENT_BODY=$(echo -e "ESLint frontend results:\n\n\`\`\`log\n$(cat lint-frontend-result.log)\n\`\`\`")
              COMMENT_BODY_JSON=$(jq -n --arg body "$COMMENT_BODY" '{"body": $body}')
              curl -X POST \
                -H "Authorization: token $GITHUB_TOKEN" \
                -H "Content-Type: application/json" \
                -d "$COMMENT_BODY_JSON" \
                "https://api.github.com/repos/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}/issues/${CIRCLE_PULL_REQUEST##*/}/comments"
            fi

      - run:
          name: Return success or failure
          command: |
            echo "Returning success or failure..."
            if [ $TEST_RESULT -ne 0 ]; then
              echo "Test failed"
              exit 1
            else
              echo "Test succeeded"
              exit 0
            fi

  test_backend:
    executor: node-executor
    # ~~省略~~

workflows:
  lint-and-test:
    jobs:
      - lint_frontend
      - test_backend
```

パイプラインが実行されるとCircleCIコンテナが立ち上がり、`config.yml`の処理を実行したあとコンテナが終了する流れになります。

2024年現在、versionは2.1
docker imageは`circleci/`ではなく`cimg/`など最新のものを使いましょう。
node:xx.x.xのところは開発環境のバージョンと合わせることをおすすめします。

上記例だとworkflowsのjobsが2つあるので、2つがそれぞれ並列実行される形になります。

DRY（重複を防ぐ）に書くにはparametersを使用したり、ファイルが肥大化しないようShell Script化してファイル分割をしたりするのがいいですが、今回はせず別記事でリファクタリングについて解説できたらと思います。

Lintやテストをただ実行するだけだとエラーがあった場合にそのままパイプラインが終了してしまいPRへのコメント送信ができなくなってしまうため、
logに実行結果詳細を保存、一旦実行結果はtrueとして扱ってからTEST_RESULTに本来の実行結果を格納し、結果次第でPRコメント送信処理をするという流れにすることで柔軟かつ求めている動作を実現しました。

コメント送信のところはCircleCIで使える定義済み環境変数を活用してます。
https://circleci.com/docs/ja/variables/#built-in-environment-variables

### 躓いた部分と対処法
開発環境のコンテナにてLintなどを実行したときとCircleCIのコンテナにて実行したときで違う結果（エラー）になる
　→ 開発環境コンテナが立ち上がるときと同じコマンドなどを使うのは前提として、さらに以下のような依存関係周りを解決してあげる対応が必要
* `.eslintrc`に`root: true,`を追加（特にモノレポの場合、特定のディレクトリでのみESLintの設定を適用）
* Cannot find module 'source-map'のエラーの対処として、`package.json`の記載修正

```json
"moduleDirectories": [
  "node_modules",
  "<rootDir>"
]
```


### デバッグ方法
pushして確認することもできますが、pushせずローカル上で動作確認することも可能です。

```sh
brew install circleci
circleci config validate
circleci local execute job名

# 環境変数の指定
circleci local execute job名 \
-e KEY=VALUE \
-e ..
```
https://circleci.com/docs/ja/local-cli/#macos-install-with-homebrew
https://circleci.com/docs/ja/how-to-use-the-circleci-local-cli/

スリープ処理を入れることで、コンテナを立ち上げさせたままコンテナ内に入って手動でコマンドを実行したりすることが可能です。10分経過すると強制的にコンテナが閉じます。

```yaml
      - run:
          name: Sleep
          command: |
            echo "Sleep.."
            sleep 600
```

```sh
# 立ち上がったCircleCIコンテナのIDを確認
docker ps
# コンテナに入る
docker exec it コンテナID /bin/bash
```


## 最後に
CircleCIを利用したテストの自動化の導入によって、手動作業の削減と品質向上に大きく貢献することができました。今回紹介した設定が皆さんの開発プロセスに役立てば幸いです。是非、ご自身のプロジェクトに適用してみてください。