[GitHub Actions] Improve Jest test performance in GitHub Actions

2024-11-11 hit count image

Let's see how to improve the performance of the Action that runs Jest in GitHub Actions.

Outline

Recently, the number of test codes has increased in our company, and the Action that checks the code in GitHub Actions has taken a long time. In this blog post, I would like to share how I improved the performance of the Action that runs Jest.

Problem

Currently my team uses the following Action to check PR.

jobs:
  check-code:
    name: Check Code
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Install dependencies
        uses: ./.github/actions/install_dependencies
      - name: Prettier
        run: yarn format
      - name: CSpell
        run: yarn cspell
      - name: ESLint
        run: yarn lint
      - name: Stylelint
        run: yarn stylelint
      - name: Test
        run: |
          yarn test:ci
          yarn test:storybook
      - name: Build
        run: |
          yarn build
      - name: Build SCSS
        run: |
          yarn typegen:scss

          # Get the changed files
          CHANGED_FILES=$(git diff --name-only HEAD)
          # Check if there are changes in the generated files
          if [ -n "$CHANGED_FILES" ]; then
            echo "Error: There are changes in the following files: $CHANGED_FILES"
            exit 1
          fi

When a PR is created, the Prettier, CSpell, ESLint, Stylelint, Test, Build, and Build SCSS are executed. This Action takes about 25 minutes like the following.

GitHub Actions - Improve Jest test performance

Performance Improvement

GitHub Actions that take 25 minutes every time a PR is created are very inefficient. In this blog post, I will introduce how to improve the performance of the Action that runs Jest.

Cache Dependencies

The first thing I did to improve the performance of the Action that runs Jest was to cache the Dependencies installed by running yarn install. This reduces the time it takes to reinstall Dependencies.

This part can be used in other Actions, so I created a separate Composite Action. If you want to know more about Composite Action, please refer to the following link.

I used actions/cache to cache the Dependencies.

The Composite Action that caches Dependencies using actions/cache is as follows.


name: 'Install Dependencies'
description: 'Install Dependencies'
runs:
  using: 'composite'
  steps:
    - name: Setup node
      uses: actions/setup-node@v4
      with:
        node-version: 20.3.0
    - name: Install dependencies
    - name: Enable Yarn 3.7.0
      shell: bash
      run: corepack enable
    - name: Get yarn cache directory path
      shell: bash
      id: yarn-cache-dir-path
      run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
    - uses: actions/cache@v4
      id: yarn-cache
      with:
        path: |
          node_modules
          **/node_modules
          ${{ steps.yarn-cache-dir-path.outputs.dir }}
        key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
        restore-keys: |
          ${{ runner.os }}-yarn-
    - name: Install dependencies
      if: steps.yarn-cache.outputs.cache-hit != 'true'
      shell: bash
      run: yarn install --frozen-lockfile

Our team uses Yarn version 3.7.0. So I added corepack enable and cached the cache folder of Yarn 3.7.0 and node_modules together.

Also, since the project is a monorepo, I cached the node_modules in the subprojects using **/node_modules.

If you are not using a monorepo and are not using Yarn 3.7.0, please refer to the official document and make appropriate settings.

After caching the Dependencies, the performance was slightly improved.

  • Before: 1m 25s
GitHub Actions - Before using actions/cache for dependencies
  • After: 9s
GitHub Actions - After using actions/cache for dependencies

Separate Actions

I executed Prettier, CSpell, ESLint, Stylelint, Test, Build, and Build SCSS in one Action. Among them, the Test part took the longest time.

So I separated the Actions so that other Actions can be performed while Test is running.

jobs:
  cspell:
    name: CSpell
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Install dependencies
        uses: ./.github/actions/install_dependencies
      - name: CSpell
        run: yarn cspell
  remark:
    if: contains(github.head_ref, 'service_1') || contains(github.head_ref, 'npm_and_yarn') || contains(github.head_ref, 'github_actions') || contains(github.head_ref, 'components') || contains(github.head_ref, 'config') || contains(github.head_ref, 'common')
    name: Remark-lint
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Install dependencies
        uses: ./.github/actions/install_dependencies
      - name: Remark-lint
        run: yarn remark:service_1
  eslint:
    if: contains(github.head_ref, 'service_1') || contains(github.head_ref, 'npm_and_yarn') || contains(github.head_ref, 'github_actions') || contains(github.head_ref, 'components') || contains(github.head_ref, 'config') || contains(github.head_ref, 'common')
    name: ESLint
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Install dependencies
        uses: ./.github/actions/install_dependencies
      - name: ESLint
        run: yarn lint:service_1
  stylelint:
    if: contains(github.head_ref, 'service_1') || contains(github.head_ref, 'npm_and_yarn') || contains(github.head_ref, 'github_actions') || contains(github.head_ref, 'components') || contains(github.head_ref, 'config') || contains(github.head_ref, 'common')
    name: Stylelint
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Install dependencies
        uses: ./.github/actions/install_dependencies
      - name: Stylelint
        run: yarn stylelint:service_1
  build:
    if: contains(github.head_ref, 'service_1') || contains(github.head_ref, 'npm_and_yarn') || contains(github.head_ref, 'github_actions') || contains(github.head_ref, 'components') || contains(github.head_ref, 'config') || contains(github.head_ref, 'common')
    name: Build
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Install dependencies
        uses: ./.github/actions/install_dependencies
      - name: Build
        run: yarn build:service_1
  test-service_1:
    if: contains(github.head_ref, 'service_1') || contains(github.head_ref, 'npm_and_yarn') || contains(github.head_ref, 'github_actions') || contains(github.head_ref, 'components') || contains(github.head_ref, 'config') || contains(github.head_ref, 'common')
    name: Test service_1
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Install dependencies
        uses: ./.github/actions/install_dependencies
      - name: Test
        run: yarn test:ci:service_1

Since we are using a monorepo, GitHub Actions like this exist for each service, and I used an if statement to run only the service.

Before separating the Actions, this Action took about 25 minutes.

GitHub Actions - Before sperating actions

After separating the Actions, this Action took about 13 minutes.

GitHub Actions - After sperating actions

Jest bail option

By using the bail option of Jest, you can stop the test if one of the tests fails.

By adding this option, you can stop all tests immediately when a test fails, so you can save time.

To set the bail option of Jest, open the package.json file that contains the command to run jest and modify it as follows.

{
  ...
  "scripts": {
    ...
    "test:ci": "jest --ci --bail"
  },
  ...
}

Jest shard option

By using the shard option of Jest, you can run tests in parallel. To run tests in parallel using the shard option, open the Action that runs Jest and modify it as follows.

  ...
  test-service_1:
    if: contains(github.head_ref, 'service_1') || contains(github.head_ref, 'npm_and_yarn') || contains(github.head_ref, 'github_actions') || contains(github.head_ref, 'components') || contains(github.head_ref, 'config') || contains(github.head_ref, 'common')
    name: Test service_1
    runs-on: ubuntu-latest
    strategy:
      matrix:
        shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    timeout-minutes: 30
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Install dependencies
        uses: ./.github/actions/install_dependencies
      - name: Test
        run: yarn test:ci:service_1 -- --shard=$/10

This project uses Turborepo to manage the monorepo.

The Action that runs yarn test:ci:service_1 in the Action is executed by running the turbo test:ci --parallel --filter=service_1 command. So I used -- to pass the option to use the --shard option of Jest. So, to pass the shard option, I used --.

Before using the shard option, the test took about 13 minutes.

GitHub Actions - After sperating action

After using the shard option, the test took about 2~3 minutes.

GitHub Actions - Use Jest shard option

Completed

Done! We’ve seen how to improve the performance of the Action that runs Jest. Before the performance improvement, it took about 25 minutes, but after the performance improvement, it took about 2~3 minutes.

If you have a similar problem, I hope you can improve the performance of the Action that runs Jest by caching Dependencies, separating Actions, and using the shard option.

Was my blog helpful? Please leave a comment at the bottom. it will be a great help to me!

App promotion

You can use the applications that are created by this blog writer Deku.
Deku created the applications with Flutter.

If you have interested, please try to download them for free.

Posts