ソフトウェアテストは、アプリケーション開発で避けて通れません。開発の初期段階でエラーを検出、修正することで、復元力のある質の高いコードを維持することができます。
JavaScriptのテストツールは多数ありますが、中でもJestは人気の高い選択肢です。Meta社の製品であるJestには、JavaScriptアプリとJavaScriptフレームワークで構築されたアプリケーション向けの広範なテスト機能が組み込まれています。
Jestの概要と主な機能、そして開発ワークフローにJestを統合する方法を掘り下げていきましょう。
Jestとは
Jestは柔軟なフレームワークで、使い方も簡単です。JavaScriptをテストするコア機能に加えて、Babel、webpack、Vite、Parcel、またはTypeScriptベースのアプリケーションをテストするための設定やプラグインがあります。
多くの開発者がJestを採用しており、コミュニティによって構築とメンテナンスが行われている数々のプラグインも魅力です。何と言っても、Jestの使いやすさは他のフレームワークと一線を画しており、JavaScriptのテストに設定やプラグインはいりません。JavaScriptフレームワークをテストする場合は、必要に応じてより高度なテストを実施することもできます。
JavaScriptプロジェクトでJestをセットアップする方法
既存のJavaScriptプロジェクトでJestをセットアップする方法を見ていきます。
前提条件
これからご紹介する手順では、以下がインストールされていることを前提とします。
Jestパッケージのインストール
- プロジェクトをまだお持ちでない場合は、こちらのリポジトリを使用してください。
starter-files
ブランチは、アプリケーションをビルドするための基盤となります。main
ブランチでコード全体を表示し、コードを照らし合わせてみてください。
- ターミナルでプロジェクトディレクトリに移動し、次のコマンドで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
─設定した回数の失敗の後に実行を停止(デフォルトでは、すべてのテストを実行して結果にエラーが表示される)verbose
─true
に設定すると、テスト実行中に個々のテストレポートを表示
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()
は、文字列を特定の長さにカットし、省略記号を追加する関数です。
テストの記述
- テストファイル「string-format.test.js」を作成します。
- 識別しやすいよう、string-format.test.jsは、string-format.jsファイルと同じディレクトリ、または特定のテストディレクトリに置いてください。テストファイルはプロジェクト内のどこにあっても問題なく、様々なシナリオでアプリケーションをテストできます。
- 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
コマンドを定義します。
- package.jsonファイルに以下の
test
スクリプトを追加します。
"scripts": {
"test": "jest"
}
npm run test
、npm test
、npm t
をターミナルで実行すると、Jestが起動します。
テストを実行すると、以下のようになります。
1つのテストスイート(string-format.test.jsファイル)、正常に実行された1つのテスト("truncates a string correctly"
)、および設定で定義したdisplayName
(Ecommerce
)が表示されます。
- string-format.jsで余分なピリオドを追加してしまうと、テストに失敗します。
これは、truncate
関数に誤りがあるか、テストの更新が必要な変更が行われていることを示唆しています。
Jestでテストを記述する方法
Jestのテスト構文
Jestの構文はシンプルで、グローバルなメソッドとオブジェクトがあります。主な関数には、describe
、test
、expect
、そしてマッチャーがあります。
describe
─関連するテストをファイルにグループ化test
─テストを実行(it
のエイリアスで、テストしたい値のアサーションが含まれる)expect
─さまざまな値に対するアサーションを宣言(さまざまな形式のアサーションに対応するマッチャーへのアクセスを提供)- マッチャー─さまざまな方法で値をアサーション。値や真偽値の等価性、コンテキストの等価性(配列に値が含まれているかどうかなど)を確認できる。
これらの関数の使用方法を具体的に見てみます。
- 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...")
})
})
- コードを実行します。
すると、以下のような結果が表示されます。
上のスクリーンショットでは、describe
のラベルがブロックを作成していることがわかります。describe
は必須ではありませんが、より多くのコンテキストを持つファイルにテストをグループ化すると便利です。
テストをTest Suiteでまとめる
Jestのテストケースには、test
関数、expect
関数、マッチャーで構成され、テストケースの集まりはTest Suite(テストスイート)と呼ばれます。先ほどの例では、string-format.test.jsはstring-format.jsファイルをテストする1つのテストケースから構成されるテストスイートです。
プロジェクトに、file-operations.js、api-logger.js、number-format.jsなどのファイルがあるとしたら、それぞれfile-operations.test.js、api-logger.test.js、number-format.test.jsのようなテストスイートを作成することができます。
マッチャーを使ってシンプルなシンプルなアサーションを書く
先にtoBe
マッチャーについて触れましたが、Jestのマッチャーを使用したアサーションには、次のようなものがあります。
toEqual
─オブジェクトインスタンスのプロパティが再帰的に等しいか(「深い」等価性とも)を検証toBeTruthy
─真偽値のコンテクストで値が真であるかどうかを検証toBeFalsy
─真偽値のコンテクスト値が偽であるかどうかを検証toContain
─配列が値を含むかどうかを検証toThrow
─呼び出された関数がエラーを投げるかどうかを検証stringContaining
─文字列が部分文字列を含むかどうかを検証
いくつか具体例を挙げてみます。
関数やコードが特定のプロパティや値を持つオブジェクトを返すことをテストしたいとします。
- 次のコードを使用します。これは、返されたオブジェクトが期待するオブジェクトと等しいことを検証するものです。
expect({
name: "Joe",
age: 40
}).toBe({
name: "Joe",
age: 40
})
上ではtoBe
を使用しており、このマッチャーは深い等価性を検証しないため、テストに失敗します。
- 深い等価性の確認には、
toEqual
を使用します。
expect({
name: "Joe",
age: 40
}).toEqual({
name: "Joe",
age: 40
})
この場合は、両方のオブジェクトが深く等しい、つまり一致するためパスします。
- 定義された配列に特定の要素が含まれているかどうかをテストするには、別のマッチャーを使用します。
expect(["orange", "pear", "apple"]).toContain("mango")
toContain
は、["orange", "pear", "apple"]
配列に期待値である"mango"
が含まれていることを定義していますが、配列には含まれないため、テストに失敗します。
- 同様のテストに変数を使用します。
const fruits = ["orange", "pear", "apple"];
const expectedFruit = "mango";
expect(fruits).toContain(expectedFruit)
非同期コードのテスト
ここまでは同期コード、つまりコードが次の行を実行する前に値を返すコードをテストする方法を見てきました。Jestでは、async
、await
、Promises を使用して非同期コードをテストすることも可能です。
例えば、 apis.jsファイルにはAPIリクエストを行う関数があります。
function getTodos() {
return fetch('https://jsonplaceholder.typicode.com/todos/1')
}
getTodos
は、GET
リクエストをhttps://jsonplaceholder.typicode.com/todos/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オブジェクトにuserId
、id
、title
、completed
、description
の5つのプロパティが含まれています。
- テストを実行します。
上のスクリーンショットから分かる通り、getTodos()
のテストは失敗します。description
プロパティを期待していたものの、APIがそれを返していません。この場合、アプリケーションにそのプロパティが必要であれば、そのプロパティを含めるようにAPI管理の担当者に依頼するか、APIのレスポンスを満たすようにテストを更新します。
description
プロパティのアサーションを削除し、テストを再実行します。
スクリーンショットは、すべてがテストに合格したことを示しています。
- 従来の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で関数やモジュールをモックすることで、 テストをよりうまく制御することができます。
- 例として、以下のコードを含むfunctions.jsファイルがあるとします。
function multipleCalls(count, callback) {
if (count < 0) return;
for (let counter = 1; counter <= count; counter++) {
callback()
}
}
multipleCalls
関数は、count
の値に基づいて実行されます。この関数は、コールバック関数(外部依存関係)に依存し、multipleCalls
が外部の依存関係を正しく実行しているかどうかを確認することが目的です。
- 次のコードで外部依存関係をモックし、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
プロパティには、コードが関数を呼び出す方法と返される値に関する情報が含まれます。
- テストを実行すると、以下のような結果が表示されます。
このコードでは、mockFunction
を5回呼び出しています。
モック関数は外部依存関係を模倣しており、アプリケーションが本番環境でmultipleCalls
を使用する場合、外部依存関係が何であるかは重要ではありません。ユニットテストは、あくまでmultipleCalls
が期待通りに動作するかどうかを検証するためのものです。
- モジュールをモックするには、
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
ブランチを複製すれば、依存関係をインストールしたり、設定を行ったりする手間を省くことができます。
- 上のサンプルプロジェクトを使用する場合は、次のコマンドで必要な依存関係をインストールします。
npm install --save-dev babel-jest @babel/preset-env @babel/preset-react react-testing-library
React Testing Libraryの代わりにEnzymeを使用することも可能です。
- babel.config.jsでBabelの設定を更新するか、存在しない場合はこのファイルを作成します。
module.exports = {
presets: [
'@babel/preset-env',
['@babel/preset-react', {runtime: 'automatic'}],
],
};
- 以下のコードを持つ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
に更新します。
- このコンポーネントのテストを作成します。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ノードを取得します。
最初のexpect
はgetByTestId("submit-details")
で、not
修飾子とtoBeDisabled
マッチャー(react-testing-library
提供)を使って、ボタンが無効でないことを示しています。すべてのマッチャーでnot
修飾子を使用し、マッチャーの反対をアサートします。
また、コンポーネント上でsubmit
イベントを発生させ、ボタンが無効であることを確認します。その他のカスタムマッチャーはこちらをご覧ください。
- テストを実行します。
starter-files
ブランチを複製する場合は、テストの前にnpm install
を実行し、 プロジェクトの依存関係をすべてインストールします。
カバレッジレポートの取得
Jestではカバレッジレポートも取得でき、プロジェクトのどの部分をテストしているかを確認することができます。
--coverage
オプションをJestに渡します。package.json(JavaScriptプロジェクト内)のJestスクリプトで指定し、Jestコマンドを更新します。
"scripts": {
"test": "jest --coverage"
}
npm run test
でコードをテストすると、以下のようなレポートが表示されます。
上のレポートでは、SubmitButton.jsとstring-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を統合し、開発作業を効率化する方法も見てみましょう。
ウォッチモードでテストを実行
ウォッチモードを使用すると、コードの更新時に自動的にテストを実行することができます。
- ウォッチモードを有効にするには、package.json(JavaScript プロジェクト)に
--watchAll
オプションを追加して、Jestコマンドスクリプトを更新します。
"scripts": {
"test": "jest --coverage --watchAll"
}
npm run test
を実行し、ウォッチモードでJestを起動します。
これでプロジェクトを変更するたびに、テストが実行されるように。これによって、アプリケーションをビルドするたびにフィードバックを得ることができます。
コミット前にフックを設定
Git環境では、特定のイベント(pullやpush、commitなど)が発生するたびにフックがスクリプトを実行します。コミット前にフックを設定することで、コミット前イベント(コミットする前にコードがトリガーするイベント)に対して実行するスクリプトを定義することができます。
コミットは、スクリプトがエラーを投げない場合にのみ成功します。
pre-commitの前にJestを実行することで、コミット前にテストが失敗するのを回避できます。
ghooksなど、プロジェクトにGitフックを設定するためのライブラリを使用することも可能です。
devDependencies
にghooks
をインストールします。
npm install ghooks --save-dev
- package.jsonファイルのトップレベル(JavaScriptプロジェクト内)に
configs
オブジェクトを追加します。 configs
の下にghooks
オブジェクトを追加します。
- キーが
pre-commit
で、値がjest
のプロパティを追加します。
{
…
"config": {
"ghooks": {
"pre-commit": "jest"
}
},
}
- コードをコミットします。これによりpre-commitフックがトリガーされ、Jestを実行します。
まとめ
Jestを開発ワークフローに統合し、更新するたびに自動的にテストを実行する方法をご紹介しました。このアプローチでは、継続的なフィードバックを取得することができるため、変更を本番環境に反映する前に、コードの問題を速やかに修正することができます。
Kinstaのアプリケーションホスティングなら、Google Cloud PlatformのプレミアムティアネットワークとC2マシン上に構築された、高速でセキュアなインフラストラクチャでアプリをデプロイ可能です。さらに、世界37箇所のデータセンターと260+のPoPを誇るHTTP/3対応CDNも利用できます。
さらに、隔離されたコンテナ技術、2つの強力なファイアウォール、Cloudflareの高度なDDoS対策により、最高レベルの安全性を確保。Kinsta APIでアプリを統合したり、ワークフローを自動化したりすることもできます。
Jestをセットアップし、Kinstaで運用して、JavaScriptアプリケーションを次のレベルに引き上げましょう。
コメントを残す