💯

【テストフレームワーク】Jestとは【テスト自動化】

に公開

Jestとは

Facebookが開発したJavaScriptのテストフレームワーク
特にReactアプリケーションのテストに適しており、簡単にセットアップできることから広く使用されている
Jestは、ユニットテスト、統合テスト、エンドツーエンドテストなど、さまざまな種類のテストをサポートしている

メリット

1. 簡単なセットアップ

  • 初期設定がシンプルで、新しいプロジェクトにも既存のプロジェクトにも簡単に導入可能

2. 豊富な機能

  • モック機能やスナップショットテストなど、テストを書くための豊富な機能が用意されている

3. 高速な実行

  • 並列テストの実行やキャッシュ機能により、テストの実行が高速

4. 直感的なAPI

  • 分かりやすく、直感的に使えるAPIが提供されている

5. 堅牢なエコシステム

  • React、Angular、Vue, Node(ExpressやNest)など、さまざまなJavaScriptライブラリやフレームワークと統合可能

そもそもテストフレームワークのメリット

  1. 自動化:
    テストフレームワークを使用することで、テストを自動的に実行できる
    手動でテストを実行するのと違い、毎回同じ結果を得ることができ、人力による漏れのリスクを減らす

  2. 効率性:
    自動化されたテストは手動テストよりもはるかに速く実行されるため、
    大規模なプロジェクトでは特に恩恵を受けることができる

  3. 一貫性:
    自動化テストは常に同じ方法で実行されるため、結果が一貫している
    一方手動でのテストは、テスターごとに手順や解釈が異なることがあり、一貫性に欠けることがある

  4. 再利用性:
    テストスクリプトは再利用可能
    そのため同じテストを何度でも実行でき、新しい機能の追加やコードの変更をしたときに影響がないか確認できる

  5. カバレッジ:
    自動テストを使用すると、カバレッジを容易に計測できる
    これにより、どの部分がテストされているかどの部分がテストされていないかを明確に把握できます

  6. ドキュメンテーション:
    テストコード自体がプロジェクトのドキュメントとして機能する
    どの機能がどのようにテストされているかを明確に記録できる

  7. 継続的インテグレーション(CI)との統合:
    テストフレームワークはCIツールと統合でき、コードがプッシュされるたびに自動でテストが実行される
    これにより、バグを早期に発見し、迅速に修正できる

テストの種類と範囲、使い道

  1. 単体テスト
      関数やメソッド、小さなクラスなどに用いられる
      コード内でテストされ、他のシステムとは独立してテストされる
      コードの各単位が期待通りに動くかの確認のために用いる
  1. 結合テスト
      ミドルウェアが正しく実行され、コントローラーが呼び出されることを確認するときなど、
      2つ以上のユニットが正しく動作するかの確認に用いられる
  1. エンドツーテスト
      いわゆる機能テスト
    クライアントのブラウザ動作から、サーバーからのレスポンスを受け取り表示するまでのテスト

実際の使い方

Jestの導入

yarn add --dev jest
yarn add --dev @types/jest ts-jest

Jestを使ったテスト環境をセットアップする

npx ts-jest config:init

jest.config.tsが作成され、Typescriptの環境でJestを使えるようになる

jest.config.ts
/** @type {import('ts-jest').JestConfigWithTsJest} **/
module.exports = {
  testEnvironment: "node",
  transform: {
    "^.+.tsx?$": ["ts-jest",{}],
  },
};

package.jsonにJestの設定

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

例として、以下の関数を定義

index.ts
type Student = {
  id: number
  name: string
}
index.test.ts
const student: Student = {
  id: 20
  name: "山田太郎" 
}

最初のテストファイルを作成

app.test.ts
describe('最初のテスト', () => {
  it('最初のテストとその実装', () => {

  })
  it('最初のテストとその実装', () => {

  })
  it('最初のテストとその実装', () => {

  })
  it('最初のテストとその実装', () => {

  })
  it('最初のテストとその実装', () => {

  })
  it('最初のテストとその実装', () => {

  })
})

テストの実行

yarn test

yarn testの実行結果

yarn run v1.22.22
$ jest
 PASS  src/app.test.ts (6.236 s)
  最初のテスト
    √ 最初のテストとその実装 (3 ms)2つ目のテストとその実装 (1 ms)3つ目のテストとその実装
    √ 4つ目のテストとその実装
    √ 5つ目のテストとその実装
    √ 6つ目のテストとその実装

Test Suites: 1 passed, 1 total
Tests:       6 passed, 6 total
Snapshots:   0 total
Time:        6.5 s
Ran all test suites.
Done in 7.85s.

解説

  • PASS src/app.test.ts
    src/app.test.tsテストファイルが成功したの意
  • √ 最初のテストとその実装(3ms)
    1つ目のテストケースが成功したことを示す
     3ミリ秒かかった
  • √ 2つ目のテストとその実装(3ms)
    2つ目のテストケースが成功したことを示す
      1ミリ秒かかった
  • 以降のテストケースも成功したことが記されている
     実行にかかった時間は省略されている

ファイル形式

  1. .test.jsや.test.tsなどの形式
  2. .spec.jsや.spec.tsなどの形式
  3. __tests__の形式

Jestで重要な関数

  1. describe()
    テストケースをグループ化するために用いる
      特定の機能やモジュールに関する複数のテストケースをまとめて管理することができる
      
      ※実装例は前述のとおり
  2. it()
    個々のテストケースの定義をする

※実装例は前述のとおり
3. test()
it()とほぼ同じ役割。it()の別名として使え

  1. expect()
    テストケース内で期待される結果を定義するために使用される
      実際の結果と期待する値を比較して、テストが通過するかどうかを判断する

    例えば、ユーザ登録したときにステータスコード201が返ってくるかを確認するためなどに用いる

app.test.ts
describe('最初のテスト', () => {
  it('最初のテストとその実装', () => {
    const statusCode = 201
    expect(statusCode).toBe(201)
    expect(20 + 50).not.toBe(70)
  })
})

yarn testを実行

yarn run v1.22.22
$ jest
 FAIL  src/app.test.ts (8.348 s)
  最初のテスト
    × 最初のテストとその実装 (22 ms)

  ● 最初のテスト › 最初のテストとその実装

    expect(received).not.toBe(expected) // Object.is equality

    Expected: not 70

      4 |     expect(statusCode).toBe(201)
      5 |
    > 6 |     expect(20 + 50).not.toBe(70)
        |                         ^
      7 |   })
      8 | })

      at Object.<anonymous> (src/app.test.ts:6:25)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        9.189 s
Ran all test suites.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

解説
テストに引っかかるとこんな感じになる
2つ目のexpectの部分の結果が期待していたものじゃないよ~と書いてある
Ran all test suites: テストがすべて実行されたよの意

  1. beforeEach
    各テストケースが実行される前に、共通の準備作業を実行するために使われる

変数などの初期化、データのリセット、セットアップ作業などに用いられる

describe('Array', () => {
  let array;

  beforeEach(() => {
    array = [1, 2, 3];
  });

  test('配列の長さを確認する', () => {
    expect(array).toHaveLength(3);
  });

  test('配列に特定の要素が含まれていることを確認する', () => {
    expect(array).toContain(2);
  });
});

今回の事例の場合、beforeEach→配列の長さの確認→beforeEach→配列に特定の要素が~の順で行われる

  1. beforeAll
    describe全体の前に一度だけ実行される
describe('Database', () => {
  let db;

  beforeAll(() => {
    db = initializeDatabase();
  });

  afterAll(() => {
    db.close();
  });

  test('データベースに接続できる', () => {
    expect(db.isConnected()).toBe(true);
  });

  test('データを読み込める', () => {
    const data = db.readData();
    expect(data).toBeDefined();
  });
});

今回は2つテストがあるが、beforeAllを使っているので
beforeAll→データ接続→データ読み込みと1回だけ実行される

非同期関数のテストの書き方

async/awaitやtry/catchなどを用いて書く場合、
expect.assertionsを書くことで、テストコードの記述ミスを防ぐことができる

// sum.test.js
const sum = require('./sum');

test('sum adds two numbers correctly', async () => {
  // 期待される `expect` の呼び出し回数を指定
  expect.assertions(1);

  const result = await sum(1, 2);
  expect(result).toBe(3);
});

また、テストコードでtry/catchの例外のスローを検証する場合、try内でスローすると、
catch内でexpectの検証ができる

    test("HttpError: 500の場合、正しいエラーメッセージとステータスコードを返すテストケース", () => {
      try {
        throw new HttpError("Internal Server Error", 500);
      } catch (error) {
        if (error instanceof HttpError) {
          expect(error.message).toBe("Internal Server Error");
          expect(error.statusCode).toBe(500);
        } else {
          throw error;
        }
      }
    });

vscode拡張機能 Jest Runner の利用

ファイル名指定してテスト実行だるい...
そんな時に便利なのが、Jest Runner

ファイル左のボタンを実行すると...

こんな感じで結果が簡単にわかる

コードカバレッジとは

ソフトウェアテストにおいて、テストスイートがどの程度ソースコードをカバーしているかを示す指標
テストの網羅性を測定するためのツールとして使われる
コードカバレッジが高いほど、網羅性が高いということになる

コードカバレッジの種類

  1. ステートメントカバレッジ:

    • コード内の各ステートメント(命令)がテストされている割合を示す
    • 例:if文やforループ内のステートメントが実行されているかどうかを確認
  2. ブランチカバレッジ:

    • 条件分岐(if, forEach)の各分岐がテストされている割合を示す
    • 例:if文のtrueおよびfalseの両方のパスがテストされているかどうかを確認
  3. 関数カバレッジ:

    • コード内の各関数がテストされている割合を示す
    • 例:全ての関数が少なくとも一度は呼び出されているかどうかを確認
  4. 行カバレッジ:

    • ソースコードの各行がテストされている割合を示す
    • 例:各行が実行されているかどうかを確認

コードカバレッジの重要性

  • 品質保証:
    コードカバレッジが高いほど、ソフトウェアの品質が保証されやすくなる
    テストが多くのコードをカバーしているため、バグや不具合を早期に検出できる

  • 信頼性向上:
    カバレッジが高いと、コードの信頼性が向上し、将来的な変更や追加に対しても強固な基盤を提供

  • メンテナンス性:
    高いカバレッジは、コードのメンテナンスが容易になる要因の一つ
    変更を加えた際に、どの部分がテストされているかを把握しやすくなる

コードカバレッジの限界

  • カバレッジの過信:
    高いカバレッジ率が必ずしもバグのないコードを意味するわけではない
    カバレッジが100%でも、テストケースが不十分であれば、バグが残る可能性がある
    8割あれば大体OK

  • コストと労力:
    カバレッジを高めるためには、詳細なテストケースの作成や実行が必要となり、コストや労力がかかる

ツールと実践

  • ツール: Jest、JUnit、Istanbul、Coverage.pyなどのツールを使用して、コードカバレッジを計測する
  • 実践: カバレッジレポートを定期的に生成し、チームでレビューすることで、テストの品質を向上させる

Jestにコードカバレッジを導入

package.jsonの変更

scripts: {
 "test": "jest",
 "coverage": "jest --coverage"
}

yarn testで起動。Done

node-mocks-httpについてとその使い方

node-mocks-httpとは

Node.jsのHTTPリクエスト・レスポンスオブジェクトをモック化するライブラリ
これを用いることで、実際のHTTPリクエストやレスポンスを使わずにテストをすることができる

使い方

まずは導入

yarn add --dev node-mocks-http
import { BookController } from './../../controllers/BookController';
import { budgets } from "../mocks/budgets"
import { createRequest, createResponse } from 'node-mocks-http'

describe('BudgetController.getAll', () => {
  it('本のデータの取得', () => {
    // expect(books).toHaveLength(3)
    // //MEMO: 空データではないことを確認するテストコード
    // expect(books).not.toHaveLength(0)

    const req = createRequest({
      method: 'GET',
      url: '/api/books',
      user: { id: 1 }
    })

    const res = createResponse()

    BookController.getAll(req, res)
  })
})

テストしたいControllerのメソッド内でconsole.logをはる

console.log(req.user.id)

yarn test

$ jest
 PASS  src/tests/unit/BookController.test.ts (11.866 s)
  ● Console

    console.log
      mockの結果 1

      at Function.getAll (src/controllers/BookController.ts:7:13)


Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        13.835 s
Ran all test suites.
Done in 17.45s.

単体テストでモックデータを使う理由

  1. 独立性:
    テストが特定のデータベースに依存せず、環境に関係なく実行できる。
    データベースのセットアップや接続が不要になるため、テストの速度と効率が向上する。

  2. 再現性
    テストデータが固定されているため、テスト結果が一貫して再現される。
    テストの実行ごとにデータが変わらないため、予期せぬ結果が出ることを防ぐ。

  3. パフォーマンス
    モックデータはメモリ内で管理されるため、データベースアクセスよりも高速。
    テストが迅速に実行されるため、開発サイクルが短縮される。

  4. 信頼性
    データベース接続のエラーやネットワークの問題に左右されずにテストを実行できる。
    データベースの状態に影響されないため、テストの信頼性が向上する。

結合テストやってみた(追記予定)

エンドツーテストやってみた(追記予定)

余談

開発時にconsole.log()を残したままだったせいでテストで変な不具合が怒って時間を消耗してしまった
実装が終わったらconsole.logは消すと肝に銘じた(当たり前ではある)

Discussion