Jestではじめるテスト

本文書は、Jestを使ってテストを行うための入門ガイドです。 Jestを使ってどうやってテストするのか?といった疑問に答えます。 テストを行っていくための最初の一歩になればと思います。

それでは、さっそく学んでいきましょう!

Jestとは

Jest公式サイト: https://jestjs.io/ja

Jestは、JavaScriptのテストを行うためのフレームワークです。 2022年現在、JavaScriptを使用する多くの開発者が使用している人気のツールです。

ランキング

―― 画像の出典: 2022.stateofjs.com

事前準備

JestはNode.jsで実行します。 あらかじめNode.jsの実行環境を構築してからはじめます。

StackBlitzではじめる

次のリンクにアクセスすると、StackBlitzで新しいNode.jsの実行環境を構築できます。

Edit on StackBlitz

StackBlitzではじめる場合は、以降の準備は不要です。

ローカル環境ではじめる

ローカル環境にNode.jsの実行環境を構築する場合、まずはじめにNode.jsをインストールします。 インストール方法はNode.jsのインストール - Node.jsを使うをご参照ください。 プロジェクトの作成方法はpackage.jsonファイル - Node.jsを使うをご参照ください。

はじめてのテスト

ターミナルから npm コマンドでJestをインストールします。

npm i -D jest

または

npm install --save-dev jest

いずれかのコマンドを実行することでJestがインストールされます。 ここでインストールしたJestは、このプロジェクトの開発用の依存関係として追加されます。 つまり、これ以降このプロジェクトは npm install コマンドを実行することでJestを導入できるようになります。

インストールしたJestは、npx jest コマンドを使用することで実行できます。

npx jest

しかし、まだテストが1件も存在しないのでこのコマンドは失敗します。

$ npx jest
No tests found, exiting with code 1
Run with `--passWithNoTests` to exit with code 0
In /home/user/projects/jest-hands-on
  11 files checked.
  testMatch: **/__tests__/**/*.[jt]s?(x), **/?(*.)+(spec|test).[tj]s?(x) - 0 matches
  testPathIgnorePatterns: /node_modules/ - 11 matches
  testRegex:  - 0 matches
Pattern:  - 0 matches

実際にテストを作成し、実行していきましょう。

次のファイルを作成します。

// hello.test.js
test("1と2の合計は3です", () => {
  expect(1 + 2).toBe(3);
});

この作成した hello.test.js は、npx jest コマンドを実行するときにテストとして実行されるようになります。

$ npx jest
 PASS  ./hello.test.js
  ✓ 1と2の合計は3です (1 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.204 s
Ran all test suites.

問題なく実行できましたか? 気付いた人もいるかと思いますが、

 PASS  ./hello.test.js

とあるのは、「テスト hello.test.js が実行され、そのテストは合格 (pass) しました」ということを意味します。

このようにしてJestは簡単にテストを行うことができます。

はじめてのテストのコードの説明

テストのコードについてより詳しく説明します。

はじめてのテストのコード:

// hello.test.js
test("1と2の合計は3です", () => {
  expect(1 + 2).toBe(3);
});

このコードは、「1と2の合計は3です」というテストを意味します。 式 1 + 2 が、 3 と等しいことを検証するテストです。 下記のJestの機能が使われています。

test() 関数

テストを宣言するための関数です。

  • 第一引数には、このテストの説明を人間が読める形式で記述します
  • 第二引数には、テストの本体を記述します

expect() 関数

引数に与えた値をテストします。

.toBe() メソッド

与えた値との同一性を検証します。

このコードは、Jestの基本的な機能を確認するための極めて単純なテストですが、テスト環境自体の検証を行うことでもあります。 テスト環境の検証は、テストを行う上で最初に確認しておく重要なポイントです。

基本的な機能

Jestの機能について説明します。

テストファイルの検出

Jestは、デフォルトで下記のファイルをテストファイルとして検出します。

  • 名前の末尾に .test あるいは .spec の含まれる .js.jsx.ts.tsx ファイル
  • __tests__ ディレクトリ以下の .js.jsx.ts.tsx ファイル

テストの自動監視

--watchAll オプションを指定することで、テストファイルの変更を自動で監視します。

npx jest --watchAll

終了するには、キーボードの q を押します。

プロジェクトでのテストコマンドの設定

この設定を行うと、npm test コマンドでJestを実行できるようになります。

package.jsonscripts プロパティの中を下記のように変更します。

{
  "scripts": {
    "test": "jest"
  }
}

NPMコマンドでのテストの実行:

npm test

npx jest コマンドの実行と同様のテスト結果が得られます。

ECMAScriptモジュールのテスト ―― Babelの設定

ECMAScriptモジュール (ESM) とは、JavaScriptをモジュールとして再利用できるようにするための仕組みです。

Node.js標準でESMを取り扱えるようにするためには package.json ファイルに "type": "module" プロパティを加えます。

{
  "type": "module"
}

このように書き加えると、プロジェクトの .js ファイルはESMとして取り扱われます。

一方、Jestに関しては、2023年3月現在、Node.js標準のESMをサポートしていません。 そのため、JestでESMをテストするには、さらにJavaScriptのコードを変換するための設定を行う必要があります。

ESMのJavaScriptのコードを変換するには、たとえば、下記の方法があります。

  • babel-jest … Babelを使用して変換 (Jestのデフォルト)
  • ts-jest … tscを使用して変換
  • esbuild-jest … esbuildを使用して変換
  • @swc/jest … swcを使用して変換

ここでは、Babelを使用して変換する方法を説明します。

まず、@babel/preset-envnpm コマンドによってインストールし、Babelの設定を行います。

npm i -D @babel/preset-env

Babelには、調整済みの設定を利用するためのプリセットと呼ばれるパッケージがあります。 @babel/preset-env は、Babelが公式で提供する一般的なJavaScriptの変換を行うためのプリセットです。

babel.config.json を作成し、下記の設定を書き加えます。

{
  "presets": ["@babel/preset-env"]
}

Babelを使用するプロジェクトは、babel.config.json というファイルなどによって設定を行います。 より詳しい設定項目に関する情報は Babel公式ドキュメント > Config Reference をご参照ください。

これでBabelの設定ができたので、JestからESMをテストできるようになりました。それではさっそくテストしてみましょう。

テストの実践 ――「うるう年」問題

ここでは「うるう年」を判定するESMを作成します。 「うるう年」の判定は、通常広く使われている date-fns などのNPMパッケージを使用することが多いですが、ここではテストを学ぶためにあえて自分で実装します。 設計して、テストを書き、コードを書くという一連のステップでより実践的なテストとの付き合い方を学びましょう。

目標の決定

まず「何を作るか」明らかにしましょう。 何を作るか曖昧なまま、ただ無為にソフトウェア開発を進めるとムダを生む恐れがあります。 ムダを生まないためにできるだけ「何を作るか」を明確にしておきましょう。

「うるう年」を判定するということは、「西暦年号がうるう年ならば true を返し、そうでなければ false を返す関数」ということと決めます。

「うるう年」とは何であるかは、ここでは日本の法令を参考にして決めます。 日本の法令上の取り扱いは、明治時代に制定された「閏年ニ関スル件」によって決められています。

神武天皇即位紀元年数ノ四ヲ以テ整除シ得ヘキ年ヲ閏年トス但シ紀元年数ヨリ六百六十ヲ減シテ百ヲ以テ整除シ得ヘキモノノ中更ニ四ヲ以テ商ヲ整除シ得サル年ハ平年トス

―― https://elaws.e-gov.go.jp/document?lawid=131IO0000000090

「神武天皇即位紀元」は、通常の西暦年号でいう紀元前660年を元年とした暦を意味します。したがって「紀元年数ヨリ六百六十ヲ減シテ」とあるのは通常の西暦年号を意味します。 端的に言うと「グレゴリオ暦法に基づいています」ということを意味します。 このままだと大変読みにくいですね。 書き換えると下記のようになります。

西暦年号が4で割り切れる年はうるう年。ただし、西暦年号が100で割り切れる年のうち、100で割った商が4で割り切れない年はうるう年ではない。

これを「うるう年」とします。整理するとこうなります。

  • 西暦年号が4で割り切れる年はうるう年
    • たとえば、西暦2024年、2028年、2032年は4で割り切れるので、うるう年です。
  • 西暦年号が4で割り切れない年はうるう年でない
    • たとえば、西暦2021年、2022年、2023年は4で割り切れないので、うるう年ではありません。
  • ただし、西暦年号が100で割り切れる年はうるう年でない
    • たとえば、西暦2100年、2200年、2300年は100で割り切れるので、うるう年ではありません。
  • ただし、西暦年号が400で割り切れる年はうるう年
    • たとえば、西暦2000年、2400年、2800年は400で割り切れるので、うるう年です。

これで「何を作るか」ということが明らかになりました。 それでは、順番にテストとコードを書いていきましょう。

「西暦年号が4で割り切れる年はうるう年」

ファイル isLeapYear.test.js を作成します。 「何を作るか」ということを忘れないようにコメントに転載します。

// isLeapYear.test.js
/** TODO:
西暦年号が4で割り切れる年はうるう年
  たとえば、西暦2024年、2028年、2032年は4で割り切れるので、うるう年です。
西暦年号が4で割り切れない年はうるう年でない
  たとえば、西暦2021年、2022年、2023年は4で割り切れないので、うるう年ではありません。
ただし、西暦年号が100で割り切れる年はうるう年でない
  たとえば、西暦2100年、2200年、2300年は100で割り切れるので、うるう年ではありません。
ただし、西暦年号が400で割り切れる年はうるう年
  たとえば、西暦2000年、2400年、2800年は400で割り切れるので、うるう年です。
*/

うるう年であることを判定するので isLeapYear という名前に決めました。 この名前のモジュールと関数を作成することに決めます。

テストを書いていきましょう。

// isLeapYear.test.js
test("西暦年号が4で割り切れる年はうるう年", () => {
  expect(isLeapYear(2024)).toBe(true);
});

これをテストし、失敗することを確認します。 この失敗は、テスト環境自体の検証を行うことでもあります。

NPMコマンドでのテストの実行:

npm test

テストの自動監視:

npx jest --watchAll

テスト結果:

 FAIL  ./isLeapYear.test.js
  ✕ 西暦年号が4で割り切れる年はうるう年 (1 ms)

失敗しますね。 この失敗によって、次の2点を実証できました。

  • 目標の「西暦年号が4で割り切れる年はうるう年」というテストが実行されること
  • 未実装のコードが意図せず合格 (pass) しないということ

テスト環境の検証は、テストを行う上での重要なポイントです。

それでは、関数を実装していきましょう。 最初からすべての実装を書こうとせず、小さい変更のみで済ませるのがポイントです。

// isLeapYear.js
function isLeapYear(year) {
  return year % 4 === 0;
}

export default isLeapYear;

ファイルを作成したら、テスト側で import 文によって実装した関数を読み込みます。

// isLeapYear.test.js
import isLeapYear from "./isLeapYear";

test("西暦年号が4で割り切れる年はうるう年", () => {
  expect(isLeapYear(2024)).toBe(true);
});

テストを実行します。

テスト結果:

 PASS  ./isLeapYear.test.js
  ✓ 西暦年号が4で割り切れる年はうるう年 (1 ms)

これでテストは合格しました。 念の為、西暦2024年のケースだけでなくほかのケースもテストしてみましょう。

「西暦年号が4で割り切れる年はうるう年」という目標を達成したと判断したら、コメントからは消しておきます。

// isLeapYear.test.js
import isLeapYear from "./isLeapYear";

test("西暦年号が4で割り切れる年はうるう年", () => {
  expect(isLeapYear(2024)).toBe(true);
  expect(isLeapYear(2028)).toBe(true);
  expect(isLeapYear(2032)).toBe(true);
});

/** TODO:
西暦年号が4で割り切れない年はうるう年でない
  たとえば、西暦2021年、2022年、2023年は4で割り切れないので、うるう年ではありません。
ただし、西暦年号が100で割り切れる年はうるう年でない
  たとえば、西暦2100年、2200年、2300年は100で割り切れるので、うるう年ではありません。
ただし、西暦年号が400で割り切れる年はうるう年
  たとえば、西暦2000年、2400年、2800年は400で割り切れるので、うるう年です。
*/

次の目標「西暦年号が4で割り切れない年はうるう年でない」に進めていきます。

テストを書き、実行します。

必要に応じて実装を修正します。

これらのテストも問題なく合格するようになれば、「西暦年号が4で割り切れない年はうるう年でない」という目標も達成したと判断して、コメントから消しておきます。

// isLeapYear.test.js
import isLeapYear from "./isLeapYear";

test("西暦年号が4で割り切れる年はうるう年", () => {
  expect(isLeapYear(2024)).toBe(true);
  expect(isLeapYear(2028)).toBe(true);
  expect(isLeapYear(2032)).toBe(true);
});

test("西暦年号が4で割り切れない年はうるう年でない", () => {
  expect(isLeapYear(2021)).toBe(false);
  expect(isLeapYear(2022)).toBe(false);
  expect(isLeapYear(2023)).toBe(false);
});

/** TODO:
ただし、西暦年号が100で割り切れる年はうるう年でない
  たとえば、西暦2100年、2200年、2300年は100で割り切れるので、うるう年ではありません。
ただし、西暦年号が400で割り切れる年はうるう年
  たとえば、西暦2000年、2400年、2800年は400で割り切れるので、うるう年です。
*/

続きの課題

残りの目標に関しても同様に進めていきましょう。

  • ただし、西暦年号が100で割り切れる年はうるう年でない
    • たとえば、西暦2100年、2200年、2300年は100で割り切れるので、うるう年ではありません。
  • ただし、西暦年号が400で割り切れる年はうるう年
    • たとえば、西暦2000年、2400年、2800年は400で割り切れるので、うるう年です。

Jestの機能

Jestの代表的な機能を紹介します。

Matcher

マッチャー (matcher) とは、与えた値を検証するためのメソッドです。

同一性の検証

  • toBe … 与えた値との同一性 (===) を検証します
  • toEqual … オブジェクトまたは配列のすべてのプロパティの同一性を再帰的に検証します
  • not … 検証の結果を反転させます

真偽値とそれに類する値の検証

  • toBeNull … null
  • toBeUndefined … undefined
  • toBeDefined … undefined でない (つまり not.toBeUndefined と等価)
  • toBeTruthy … if ステートメントが真であるとみなすもの
  • toBeFalsy … if ステートメントが偽であるとみなすもの

数値

  • toBeGreaterThan … >
  • toBeGreaterThanOrEqual … >=
  • toBeLessThan … <
  • toBeLessThanOrEqual … <=
  • toBeCloseTo … 浮動小数点数の丸め誤差を考慮した同一性

文字列

  • toMatch … 正規表現のパターン

配列と反復可能なオブジェクト

  • toContain … 配列や反復可能なオブジェクトに特定のアイテムが含まれているかを検証します

例外

  • toThrow … 例外を発報するかどうかを検証します

その他

より詳しい情報は Jest公式リファレンス をご参照ください。

Promise

Jestは、test に渡す関数の前に async キーワードを記述するだけで、非同期テストを実行できます。

より詳しい情報は Jest公式ドキュメント Async/Await をご参照ください。

beforeEach と afterEach

beforeEachafterEach を使用することでテストの実行の前に繰り返し行う準備や、後片付けの処理を宣言できます。

より詳しい情報は Jest公式ドキュメント セットアップと破棄 をご参照ください。

モック

モック関数を使用することでコード間の繋がりをテストできます。

より詳しい情報は Jest公式ドキュメント モック関数 をご参照ください。

テストの作法

テストを書くときの代表的な作法を紹介します。

Arrange・Act・Assert (AAA) パターン

テストを書くときの作法の1つです。 準備 (Arrange)・実行 (Act)・検証 (Assert) というプロセスで分けて書きます。 準備・実行・検証をそれぞれ分けて書いておくことで比較的読みやすいテストを書くことができます。

例:

test("正しくJSONをパースできる", () => {
  // 準備
  const json = `{ "name": "太郎", "age": 20 }`;

  // 実行
  const parsed = JSON.parse(json);

  // 検証
  expect(parsed).toEqual({ name: "太郎", age: 20 });
});

参考文献・動画

Jest

Jest公式サイト

テスト駆動開発

和田卓人 (2010)「TDD のこころ」

和田卓人 (2020)「TDD Boot Camp 2020 Online #1 基調講演/ライブコーディング」

安井力 (2021)「『テスト自動化とテスト駆動開発』講演動画」

質問・提案・問題の報告

もし何か気になることがあれば、GitHub Issues からお気軽にお寄せください。