ソフトウェアテストは、アプリケーション開発で避けて通れません。開発の初期段階でエラーを検出、修正することで、復元力のある質の高いコードを維持することができます。

JavaScriptのテストツールは多数ありますが、中でもJestは人気の高い選択肢です。Meta社の製品であるJestには、JavaScriptアプリとJavaScriptフレームワークで構築されたアプリケーション向けの広範なテスト機能が組み込まれています。

Jestの概要と主な機能、そして開発ワークフローにJestを統合する方法を掘り下げていきましょう。

Jestとは

Jestは柔軟なフレームワークで、使い方も簡単です。JavaScriptをテストするコア機能に加えて、Babel、webpack、ViteParcel、またはTypeScriptベースのアプリケーションをテストするための設定やプラグインがあります。

多くの開発者がJestを採用しており、コミュニティによって構築とメンテナンスが行われている数々のプラグインも魅力です。何と言っても、Jestの使いやすさは他のフレームワークと一線を画しており、JavaScriptのテストに設定やプラグインはいりません。JavaScriptフレームワークをテストする場合は、必要に応じてより高度なテストを実施することもできます。

JavaScriptプロジェクトでJestをセットアップする方法

既存のJavaScriptプロジェクトでJestをセットアップする方法を見ていきます。

前提条件

これからご紹介する手順では、以下がインストールされていることを前提とします。

Jestパッケージのインストール

  1. プロジェクトをまだお持ちでない場合は、こちらのリポジトリを使用してください。

starter-filesブランチは、アプリケーションをビルドするための基盤となります。mainブランチでコード全体を表示し、コードを照らし合わせてみてください。

  1. ターミナルでプロジェクトディレクトリに移動し、次のコマンドでJestをnpmでインストールします。
npm install --save-dev jest

--save-devは、devDependenciesの下に開発に必要な依存関係を含むパッケージをインストールするようnpmに指示するものです。

Jestの設定

Jestは基本的に設定なしで動作しますが、package.jsonファイルと設定ファイル(jest.config.js)を使って機能を強化することができます。

package.jsonファイルでの設定

package.jsonファイルに以下のようなプロパティを持つjestオブジェクトを追加します。

{
  …
  "jest": {
    "displayName": "Ecommerce",
    "globals": {
      "PROJECT_NAME": "Ecommerce TD"
    },
    "bail": 20,
    "verbose": true
  },
}

これによって、テストを行う際、Jestがこのオブジェクトを検索して設定を適用します。Jestの設定に関するドキュメントに詳細が記載されていますが、このオブジェクトのプロパティは以下のとおりです。

  • displayName─ラベルとしてテスト結果に追加される
  • globals─オブジェクトの値を保持し、テスト環境で利用可能なグローバル変数を定義
  • bail─設定した回数の失敗の後に実行を停止(デフォルトでは、すべてのテストを実行して結果にエラーが表示される)
  • verbosetrueに設定すると、テスト実行中に個々のテストレポートを表示

jest.config.jsファイルでの設定

jest.config.jsファイルで設定を行うことも可能です。Jestでは、拡張子.ts.mjs.cjs.jsonがサポートされています。テストを実行する際、Jestがこれらのファイルを探し、見つかったファイルの設定を適用します。

例えば、jest.config.jsファイルで以下のような設定を行うことができます。

const config = {
  displayName: "Ecommerce",
  globals: {
    "PROJECT_NAME": "Ecommerce TD"
  },
  bail: 20,
  verbose: true
}

module.exports = config;

上のコードは、前の例と同じプロパティを持つJest設定オブジェクトをエクスポートします。

JSONでシリアライズ可能な設定オブジェクトを含むカスタムファイルを使用し、 そのファイルパスをテスト実行時に--configへ渡すことも可能です。

基本的なテストファイルの作成

Jestの設定を終えたら、テストファイルを作成します。Jestはプロジェクトのテストファイルをレビューおよび実行し、結果を表示します。テストファイルは通常、識別しやすいよう、[名前].test.js[名前]-test.js のような形式が推奨されます。

例として、次のようなstring-format.jsファイルを作成します。

function truncate(
  str,
  count,
  withEllipsis = true
) {
  if (str.length < = count)
    return str

  const substring = str.substr(0, count)

  if (!withEllipsis)
    return substring

  return substring + '...'
}

module.exports = { truncate }

truncate()は、文字列を特定の長さにカットし、省略記号を追加する関数です。

テストの記述

  1. テストファイル「string-format.test.js」を作成します。
  1. 識別しやすいよう、string-format.test.jsは、string-format.jsファイルと同じディレクトリ、または特定のテストディレクトリに置いてください。テストファイルはプロジェクト内のどこにあっても問題なく、様々なシナリオでアプリケーションをテストできます。
  2. string-format.test.jsに、以下のような基本的なテストを記述します。
const { truncate } = require('./string-format')

test('truncates a string correctly', () = > {
  expect(truncate("I am going home", 6)).toBe('I am g...')
})

上のテストケースには、truncates a string correctly(文字列を適切に短くする)を含まれており、expect関数で値が期待される結果と一致するかどうかを確認します。

expectへの引数として、truncate("I am going home", 6)を渡します。 また、truncateを引数"I am going home"および6で呼び出し、返された値をテストします。expectの呼び出しによって、期待されるオブジェクトが返され、Matcher(マッチャー)へのアクセスが提供されます。

また、"I am g…"を引数として持つtoBeマッチャーは、期待する値と実際の値が一致するかどうかを確認するものです。

テストの実行

テストを実行するには、jestコマンドを定義します。

  1. package.jsonファイルに以下のtestスクリプトを追加します。
"scripts": {
  "test": "jest"
}
  1. npm run testnpm testnpm tをターミナルで実行すると、Jestが起動します。

テストを実行すると、以下のようになります。

string-format.test.jsのテスト結果例
string-format.test.jsのテスト結果例

1つのテストスイート(string-format.test.jsファイル)、正常に実行された1つのテスト("truncates a string correctly")、および設定で定義したdisplayNameEcommerce)が表示されます。

  1. string-format.jsで余分なピリオドを追加してしまうと、テストに失敗します。
TRUNCATEの入力ミスによるテスト結果の失敗例
TRUNCATEの入力ミスによるテスト結果の失敗例

これは、truncate関数に誤りがあるか、テストの更新が必要な変更が行われていることを示唆しています。

Jestでテストを記述する方法

Jestのテスト構文

Jestの構文はシンプルで、グローバルなメソッドとオブジェクトがあります。主な関数には、describetestexpect、そしてマッチャーがあります。

  • describe─関連するテストをファイルにグループ化
  • test─テストを実行(itのエイリアスで、テストしたい値のアサーションが含まれる)
  • expect─さまざまな値に対するアサーションを宣言(さまざまな形式のアサーションに対応するマッチャーへのアクセスを提供)
  • マッチャー─さまざまな方法で値をアサーション。値や真偽値の等価性、コンテキストの等価性(配列に値が含まれているかどうかなど)を確認できる。

これらの関数の使用方法を具体的に見てみます。

  1. string-format.test.jsファイルのテストを以下のコードに置き換えます。
describe("all string formats work as expected", () = > {
  test("truncates a string correctly", () = > {
    expect(
      truncate("I am going home", 6)
    ).toBe("I am g...")
  })
})
  1. コードを実行します。

すると、以下のような結果が表示されます。

describeラベルが表示されたテストの成功例
describeラベルが表示されたテストの成功例

上のスクリーンショットでは、describeのラベルがブロックを作成していることがわかります。describeは必須ではありませんが、より多くのコンテキストを持つファイルにテストをグループ化すると便利です。

テストをTest Suiteでまとめる

Jestのテストケースには、test関数、expect関数、マッチャーで構成され、テストケースの集まりはTest Suite(テストスイート)と呼ばれます。先ほどの例では、string-format.test.jsstring-format.jsファイルをテストする1つのテストケースから構成されるテストスイートです。

プロジェクトに、file-operations.jsapi-logger.jsnumber-format.jsなどのファイルがあるとしたら、それぞれfile-operations.test.jsapi-logger.test.jsnumber-format.test.jsのようなテストスイートを作成することができます。

マッチャーを使ってシンプルなシンプルなアサーションを書く

先にtoBeマッチャーについて触れましたが、Jestのマッチャーを使用したアサーションには、次のようなものがあります。

  • toEqual─オブジェクトインスタンスのプロパティが再帰的に等しいか(「深い」等価性とも)を検証
  • toBeTruthy─真偽値のコンテクストで値が真であるかどうかを検証
  • toBeFalsy─真偽値のコンテクスト値が偽であるかどうかを検証
  • toContain─配列が値を含むかどうかを検証
  • toThrow─呼び出された関数がエラーを投げるかどうかを検証
  • stringContaining─文字列が部分文字列を含むかどうかを検証

いくつか具体例を挙げてみます。

関数やコードが特定のプロパティや値を持つオブジェクトを返すことをテストしたいとします。

  1. 次のコードを使用します。これは、返されたオブジェクトが期待するオブジェクトと等しいことを検証するものです。
expect({
  name: "Joe",
  age: 40
}).toBe({
  name: "Joe",
  age: 40
})

上ではtoBeを使用しており、このマッチャーは深い等価性を検証しないため、テストに失敗します。

  1. 深い等価性の確認には、toEqualを使用します。
expect({
  name: "Joe",
  age: 40
}).toEqual({
  name: "Joe",
  age: 40
})

この場合は、両方のオブジェクトが深く等しい、つまり一致するためパスします。

  1. 定義された配列に特定の要素が含まれているかどうかをテストするには、別のマッチャーを使用します。
expect(["orange", "pear", "apple"]).toContain("mango")

toContainは、["orange", "pear", "apple"]配列に期待値である"mango"が含まれていることを定義していますが、配列には含まれないため、テストに失敗します。

  1. 同様のテストに変数を使用します。
const fruits = ["orange", "pear", "apple"];
const expectedFruit = "mango";

expect(fruits).toContain(expectedFruit)

非同期コードのテスト

ここまでは同期コード、つまりコードが次の行を実行する前に値を返すコードをテストする方法を見てきました。Jestでは、asyncawait、Promises を使用して非同期コードをテストすることも可能です。

例えば、 apis.jsファイルにはAPIリクエストを行う関数があります。

function getTodos() {
  return fetch('https://jsonplaceholder.typicode.com/todos/1')
}

getTodosは、GETリクエストをhttps://jsonplaceholder.typicode.com/todos/1に送ります。

  1. 偽のAPIをテストするため、以下のコードでapis.test.jsという名前のファイルを作成してみます。
const { getTodos } = require('./apis')

test("gets a todo object with the right properties", () = > {
  return getTodos()
    .then((response) = > {
      return response.json()
    })
    .then((data) = > {
      expect(data).toHaveProperty('userId')
      expect(data).toHaveProperty('id')
      expect(data).toHaveProperty('title')
      expect(data).toHaveProperty('completed')
      expect(data).toHaveProperty('description')
    })
})

このテストケースは、todoオブジェクトを取得するgetTodos関数を呼び出します。Promiseを解決すると、.then関数を使って解決された値が取得されます。

この値で、レスポンスをJSON形式に変換する別のPromise、response.json()を返します。そして別の.then関数が、expectとマッチャーを含むJSONオブジェクトを取得。このコード例では、JSONオブジェクトにuserIdidtitlecompleteddescriptionの5つのプロパティが含まれています。

  1. テストを実行します。
非同期コードのテスト結果の失敗例
非同期コードのテスト結果の失敗例

上のスクリーンショットから分かる通り、getTodos()のテストは失敗します。descriptionプロパティを期待していたものの、APIがそれを返していません。この場合、アプリケーションにそのプロパティが必要であれば、そのプロパティを含めるようにAPI管理の担当者に依頼するか、APIのレスポンスを満たすようにテストを更新します。

  1. descriptionプロパティのアサーションを削除し、テストを再実行します。
非同期コードのテストがパスしたことを示すテスト結果
非同期コードのテストがパスしたことを示すテスト結果

スクリーンショットは、すべてがテストに合格したことを示しています。

  1. 従来のPromise処理ではなく、async/awaitを使用します。
test("gets a todo object with the right properties", async () = > {
  const response = await getTodos()
  const data = await response.json()

  expect(data).toHaveProperty("userId")
  expect(data).toHaveProperty("id")
  expect(data).toHaveProperty("title")
  expect(data).toHaveProperty("completed")
})

上の例では、asyncgが関数の前にきています。getTodos()の前にawaitを使い、response.json()の前にawaitを使用しています。

Jestの高度な機能

モック関数とモジュール

テストを書く際、外部依存関係をテストしたい場合があるかもしれません。また場合によっては(特に単体テストでは)、外部からの影響を排除しなければならないことも考えられます。そのような状況では、Jestで関数やモジュールをモックすることで、 テストをよりうまく制御することができます。

  1. 例として、以下のコードを含むfunctions.jsファイルがあるとします。
function multipleCalls(count, callback) {
  if (count < 0) return;

  for (let counter = 1; counter <= count; counter++) {
    callback()
  }
}

multipleCalls関数は、countの値に基づいて実行されます。この関数は、コールバック関数(外部依存関係)に依存し、multipleCallsが外部の依存関係を正しく実行しているかどうかを確認することが目的です。

  1. 次のコードで外部依存関係をモックし、functions.test.jsテストファイルで依存関係の状態を追跡します。
const { multipleCalls } = require('./functions')

test("functions are called multiple times correctly", () => {
  const mockFunction = jest.fn()

  multipleCalls(5, mockFunction)

  expect(
    mockFunction.mock.calls.length
  ).toBe(5)
})

このコードは、jestオブジェクトのfnメソッドがモック関数を作成し、5とモック関数を引数として渡した後、multipleCallsを実行します。これは、mockFunctionが5回呼び出されることを保証するもので、mockプロパティには、コードが関数を呼び出す方法と返される値に関する情報が含まれます。

  1. テストを実行すると、以下のような結果が表示されます。
モック関数を使ったテスト結果の成功例
モック関数を使ったテスト結果の成功例

このコードでは、mockFunctionを5回呼び出しています。

モック関数は外部依存関係を模倣しており、アプリケーションが本番環境でmultipleCallsを使用する場合、外部依存関係が何であるかは重要ではありません。ユニットテストは、あくまでmultipleCallsが期待通りに動作するかどうかを検証するためのものです。

  1. モジュールをモックするには、mockメソッドを使ってファイルパスを渡します。
const {
  truncate,
} = require("./string-format")

jest.mock("./string-format.js")

このコードは、string-format.jsがエクスポートするすべての関数を模倣し、それらを呼び出す頻度を追跡します。モジュールのtruncateがモック関数になり、その関数は元のロジックを失います。truncate.mock.calls.lengthプロパティで、テストでtruncateが何回実行されたかを確認できます。

エラーが発生した、またはコードが動作しない場合は、実装の完全版を参照し比較してください。

JestとReact Testing Libraryを使ってReactコンポーネントをテストする方法

プロジェクトをまだお持ちでない場合には、Reactのサンプルプロジェクトを使用してください。starter-filesブランチは、手順に従いながらコードを書くのに便利です。また、mainブランチでコード全体を表示し、コードを照らし合わせてみてください。

Jestを使って、ReactなどのJavaScriptフレームワークをテストすることができます。Create React AppでReactプロジェクトを作成すると、React Testing LibraryとJestが自動的にサポートされます。Create React Appを使わずにReactプロジェクトを作成する場合は、BabelとReactテストライブラリを使ってテストするために、Jestをインストールしてください。starter-appブランチを複製すれば、依存関係をインストールしたり、設定を行ったりする手間を省くことができます。

  1. 上のサンプルプロジェクトを使用する場合は、次のコマンドで必要な依存関係をインストールします。
npm install --save-dev babel-jest @babel/preset-env @babel/preset-react react-testing-library

React Testing Libraryの代わりにEnzymeを使用することも可能です。

  1. babel.config.jsでBabelの設定を更新するか、存在しない場合はこのファイルを作成します。
module.exports = {
  presets: [
    '@babel/preset-env',
      ['@babel/preset-react', {runtime: 'automatic'}],
  ],
};
  1. 以下のコードを持つsrc/SubmitButton.jsファイルがあるとします。
import React, { useState } from 'react'

export default function SubmitButton(props) {
  const {id, label, onSubmit} = props
  const [isLoading, setisLoading] = useState(false)

  const submit = () => {
    setisLoading(true)
    onSubmit()
  }

  return 

SubmitButtonコンポーネントは、以下3つのpropsを受け取ります。

  • id─ボタンの識別子
  • label─ ボタンにレンダリングするテキスト
  • onSubmit─ボタンがクリックされたときにトリガーする関数

このコードでは、data-testid属性にidを割り当てます。

また、このコンポーネントはisLoadingの状態を追跡し、誰かがボタンをクリックした際にtrueに更新します。

  1. このコンポーネントのテストを作成します。SubmitButton.test.jsファイルに以下のコードを貼り付けます。
import {fireEvent, render, screen} from "@testing-library/react"
import "@testing-library/jest-dom"
import SubmitButton from "./SubmitButton"

test("SubmitButton becomes disabled after click", () => {
  const submitMock = jest.fn()

  render(
    <SubmitButton
      id="submit-details"
      label="Submit"
      onSubmit={submitMock}
    / >
  )

  expect(screen.getByTestId("submit-details")).not.toBeDisabled()

  fireEvent.submit(screen.getByTestId("submit-details"))

  expect(screen.getByTestId("submit-details")).toBeDisabled()
})

上のコードは、SubmitButtonコンポーネントをレンダリングし、screen.getByTestIdクエリを使ってdata-testid属性によりDOMノードを取得します。

最初のexpectgetByTestId("submit-details")で、not修飾子とtoBeDisabledマッチャー(react-testing-library提供)を使って、ボタンが無効でないことを示しています。すべてのマッチャーでnot修飾子を使用し、マッチャーの反対をアサートします。

また、コンポーネント上でsubmitイベントを発生させ、ボタンが無効であることを確認します。その他のカスタムマッチャーはこちらをご覧ください。

  1. テストを実行します。starter-filesブランチを複製する場合は、テストの前にnpm installを実行し、 プロジェクトの依存関係をすべてインストールします。
Reactコンポーネントのテストがパスしたことを示すテスト結果
Reactコンポーネントのテストがパスしたことを示すテスト結果

カバレッジレポートの取得

Jestではカバレッジレポートも取得でき、プロジェクトのどの部分をテストしているかを確認することができます。

  1. --coverageオプションをJestに渡します。package.json(JavaScriptプロジェクト内)のJestスクリプトで指定し、Jestコマンドを更新します。
"scripts": {
  "test": "jest --coverage"
}
  1. npm run testでコードをテストすると、以下のようなレポートが表示されます。
カバレッジレポート例
カバレッジレポート例

上のレポートでは、SubmitButton.jsstring-format.jsの関数をすべてテストしたことを示しています。また、string-format.jsのステートメントと行はテストされていないこと、string-format.jsで網羅されていない行が7行と12行であることも確認できます。

7行目、truncate 関数のreturn strは、if (str.length <= count)falseを返すため、実行されません。

12行目、truncate 関数のreturn substringは、if (!withEllipsis)falseを返すため、実行されません。

開発ワークフローにJestを統合する方法

開発ワークフローにJestを統合し、開発作業を効率化する方法も見てみましょう。

ウォッチモードでテストを実行

ウォッチモードを使用すると、コードの更新時に自動的にテストを実行することができます。

  1. ウォッチモードを有効にするには、package.json(JavaScript プロジェクト)に--watchAllオプションを追加して、Jestコマンドスクリプトを更新します。
"scripts": {
  "test": "jest --coverage --watchAll"
}
  1. npm run testを実行し、ウォッチモードでJestを起動します。
ウォッチモードでJestを実行
ウォッチモードでJestを実行

これでプロジェクトを変更するたびに、テストが実行されるように。これによって、アプリケーションをビルドするたびにフィードバックを得ることができます。

コミット前にフックを設定

Git環境では、特定のイベント(pullやpush、commitなど)が発生するたびにフックがスクリプトを実行します。コミット前にフックを設定することで、コミット前イベント(コミットする前にコードがトリガーするイベント)に対して実行するスクリプトを定義することができます。

コミットは、スクリプトがエラーを投げない場合にのみ成功します。

pre-commitの前にJestを実行することで、コミット前にテストが失敗するのを回避できます。

ghooksなど、プロジェクトにGitフックを設定するためのライブラリを使用することも可能です。

  1. devDependenciesghooksをインストールします。
npm install ghooks --save-dev
  1. package.jsonファイルのトップレベル(JavaScriptプロジェクト内)にconfigsオブジェクトを追加します。
  2. configsの下にghooksオブジェクトを追加します。
  1. キーがpre-commitで、値がjestのプロパティを追加します。
{
  …
  "config": {
    "ghooks": {
      "pre-commit": "jest"
    }
  },
}
  1. コードをコミットします。これによりpre-commitフックがトリガーされ、Jestを実行します。
ghooksを使ってコミット前にJestを実行
ghooksを使ってコミット前にJestを実行

まとめ

Jestを開発ワークフローに統合し、更新するたびに自動的にテストを実行する方法をご紹介しました。このアプローチでは、継続的なフィードバックを取得することができるため、変更を本番環境に反映する前に、コードの問題を速やかに修正することができます。

Kinstaのアプリケーションホスティングなら、Google Cloud PlatformのプレミアムティアネットワークとC2マシン上に構築された、高速でセキュアなインフラストラクチャでアプリをデプロイ可能です。さらに、世界37箇所のデータセンターと260+のPoPを誇るHTTP/3対応CDNも利用できます。

さらに、隔離されたコンテナ技術、2つの強力なファイアウォール、Cloudflareの高度なDDoS対策により、最高レベルの安全性を確保。Kinsta APIでアプリを統合したり、ワークフローを自動化したりすることもできます。

Jestをセットアップし、Kinstaで運用して、JavaScriptアプリケーションを次のレベルに引き上げましょう。

Marcia Ramos Kinsta

Kinstaのエディトリアルチームリード。大のオープンソース&コーディング好き。IT業界向けのテクニカルライティングと編集に7年以上携わり、的確かつ簡潔なコンテンツを制作しながら、チームで協力し合い、ワークフローの改善を行っている。