iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

jest基礎とテスト戦略

はじめに

こんにちは。iimonでエンジニアをしている林と申します。 本記事はiimonアドベントカレンダ11日目の記事となります。 今回テスト戦略やjestについて調べたので記事にしてみようと思います。

テスト戦略

今回挙げるテストの種類以外にも様々なテストがあると思いますが、 今回はフロントエンド開発において必ず出てくるであろう「ユニットテスト」「インテグレーションテスト」「E2Eテスト」についてのテスト戦略を解説していきます。

テストの種類

上記で挙げたテストの詳細については下記になります。

  • ユニットテスト(単体テスト)
    • 最も基本的なテスト
    • 関数やメソッドなどの単位での動作を検証する
    • 小さい単位で見ているので速度が速い
    • 全体や結合してる部分など複数の単位が合わさった時に起きてしまう不具合については検出出来ない。
  • 結合テスト(インテグレーションテスト)
    • 複数のユニットを連携させて、正しく動作をしているか検証する。
    • セットアップやデータの準備が複雑になる事がある。
    • ユニットテストの弱点を補っている。
  • E2Eテスト(エンドツーエンドテスト)(End to End)
    • アプリケーション全体を動作させ、ユーザーの操作を模倣して検証する。
    • 実行時間が長く、セットアップも複雑になる。

テスト戦略

上記での3つのテストをどれだけの頻度と深さで行うか検討するのがテスト戦略となります。

全てのコードに対して上記3種類のあらゆるテストを100%書ければ良いですが、リソースの制約から中々難しくあります。

また、テストの効果が大きい箇所もあれば小さい箇所もあるので最大限効率良くテストを書いていく為にテスト戦略が重要となります。

近年のフロントエンドでは主にTesting Trophy(テスティングトロフィー)と言うmodelを参考にプロダクトの開発状況や条件に応じてカスタマイズする事が主流との事です。

Testing Trophyのmodel図は以下の通りです(これは簡単に書いた図ですが参考資料には超絶まんまトロフィーの絵を使った図があるので興味があれば是非ご覧下さい)

このmodel図の形状がトロフィーに似ていることからTesting Trophyと名付けられました。

  • 横方向はテストの数。
  • 縦方向は実装速度や作成、メンテナンスにかかるコスト。
  • StaticとはLinterやTypeScriptの静的解析のこと。
  • 結合部分でバグが発生しやすいのでユニットテストよりもインテグレーションテストの数を一番としている。
    • ts誕生前の主流modelであるテストピラミットの場合はユニットテストの数を一番多くしていました。tsの誕生でstaticが組み込まれた事により、staticとunitで単体のバグは予め網羅出来る可能性が高い為、結合部分のtestを増やした方が良いのではないかと言う考えに恐らく至ったのでは無いかと思います。

Jestとは

  • Meta社(旧Facebook)が開発したJS,TSのテストフレームワーク
  • React, Vue, NodeでのテストなどJS,TSで書かれたコードはほぼテストが可能。

マッチャー関数

Jest では、マッチャー (matcher) を使用して、様々な方法で値のテストをすることができます。 大体のマッチャーの一覧は以下になりますが、まだまだマッチャーは沢山ありますのでもっと知りたい方は公式ドキュメントExpect(https://jestjs.io/ja/docs/expect)をご覧下さい。

  1. 一般的なマッチャー:
    • toBe: 厳密な等価性をテストします(Object.isを使用)。
    • toEqual: オブジェクトや配列の値をチェックします。
  2. 真偽値マッチャー:
    • toBeNull: nullのみに一致します。
    • toBeUndefined: undefinedのみに一致します。
    • toBeDefined: toBeUndefinedの反対です。
    • toBeTruthy: ifステートメントが真とみなす値に一致します。
    • toBeFalsy: ifステートメントが偽とみなす値に一致します。
  3. 文字列マッチャー:
    • toMatch: 文字列が正規表現にマッチするかをテストします。
  4. 配列と反復可能なオブジェクトマッチャー:
    • toContain: 配列や反復可能なオブジェクトに特定のアイテムが含まれているかをテストします。
  5. 例外マッチャー:
    • toThrow: ある関数が例外を投げるかをテストします。

ハンズオン

ここでは「toBe」「toEqual」「not」3つのマッチャーを実際にハンズオンで紹介します。

  • expect関数

    • テスト対象の値を引数として受け取り、その値に対して様々なテストを行うためのメソッドチェーンを提供します。
  • toBe

    • jsのobject.isと同等の比較が行われ、期待値がテスト対象の値と全く同じである事を確認する。
    • StringやNumberやBooleanのテストが出来る。
      test('Numberテスト', () => {
        expect(2 + 2).toBe(4)
      })
    
      test('Stringのテスト', () =>{
        expect('Jest').toBe('Jest')
      })
    
      test('Booleanのテスト', () =>{
        expect(true).toBe(true)
      })
    
  • toEqual

    • オブジェクトや配列が同じ構造を持っているか検証する際に使用される
      test('配列テスト', () => {
        const arr1 = [1, 2, 3]
        const arr2 = [1, 2, 3]
        expect(arr1).toEqual(arr2)
      })
    
      test('オブジェクトテスト', () => {
        const obj1 = {
          a: 1,
          b: 2,
        }
        const obj2 = {
          a: 1,
          b: 2,
        }
        expect(obj1).toEqual(obj2)
      })
    
  • not

    • expextとマッチャー関数の間に付けることでマッチャー関数の内容を反転させる。
      test('1+1は3では無い', () => {
        expect(1 + 1).not.toBe(3)
      })
    

それではハンズオンをしながらjestを学んで行きたいと思います。

今回はユニットテストのみを書いて行きますが、ユニット単位でテストが問題ない事が確認出来たら、それを組み合わせて結合テストを作成が可能です。

jestのインストールと設定

mkdir jest-tutorial && cd $_
code .

node.jsを初期化(package.jsonが作成される。)

npm init -y

typescriptをinstall

npm i -D typescript

tsconfig.jsonを作成

npx tsc --init

ts-jestをinstall

  • ts-jestはjestでtypescriptのコードをテストことができる。
npm i -D jest @types/jest ts-jest

必要なライブラリは入ったけど、jestはそのままだとtsのテストは出来ないので追加の設定を行う。

まずはjestの設定ファイルを作成

npx ts-jest config:init

[@package.json]

scriptのtestの項目をjestに変更する

  • これでnpm testコマンドでテストが実行できる様になる。
{
  "name": "jest-lesson",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "jest"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/jest": "^29.5.10",
    "jest": "^29.7.0",
    "ts-jest": "^29.1.1",
    "typescript": "^5.3.2"
  }
}
  • rootにsrcディレクトリとmatcher.tsと言うファイルを作成。
  • rootにtestディレクトリとmatcher.test.tsと言うファイルを作成
    • jestではテストファイルは.test.tsもしくは.spec.ts の拡張子を付けることによって自動的にテストである事を認識します。
mkdir src
touch src/matcher.ts
mkdir test
touch test/matcher.test.ts

以下の様にファイル内のコードをそれぞれ定義します。

  • sum関数
    • 引数の値を足して返します。
  • doubleArray関数
    • 引数の配列の値を2倍にして返します。
  • describe関数
    • 第一引数にstringをとり、第二引数にコールバック関数を取ります。
      • 第一引数は日本語でも英語でも大丈夫です。
      • 第二引数の中でtestを定義する事によりグループ化する事が可能です。
  • test関数
    • 第一引数にstringをとり、第二引数にコールバック関数を取ります。
    • 第二引数にテストを書く事が可能です。
    • it関数としてもテストを定義出来ます。これらは同じ意味です。
      • 同じく第一引数は日本語でも英語でも大丈夫です。

[src/matcher.ts]

export const sum = (a: number, b: number): number => {
  return a + b;
}

export const doubleArray = (arr: number[]): number[] => {
  return arr.map(x => x * 2);
}

[test/matcher.test.ts]

import { sum, doubleArray } from '../src/matcher'

describe("sum関数のテスト", () => {
  test('1+2は3になる', () => {
    expect(sum(1, 2)).toBe(3)
  })

  test('1+1は3では無い', () => {
    expect(sum(1, 1)).not.toBe(3)
  })
})

describe('doubleArray関数のテスト', () => {
  test('配列内の数値が2倍される', () => {
    const input = [1, 2, 3];
    const expectedOutput = [2, 4, 6];
    expect(doubleArray(input)).toEqual(expectedOutput);
});
})

npm testを実行すると以下の様になります。

  • describeによって纏まったグループ単位でテスト結果が表示されます。
  • passはテストが成功した事を意味していて、成功したテストのパスを示している。
  • Test Suites(スイーツ)
    • 成功したテストファイルとその合計を示している
  • Tests
    • test、it関数で作成した関数の合計と成功した関数の数値を出している。
  • Snapshots
    • jestのSnapshotで作成したtestの合計数を出している
  • Time
    • テスト全体の時間

非同期のテスト

非同期関数のテスト方法を記載していきます」。

  • jestでは非同期関数をテストする為にpromisesを使用する方法とasync awaitを使用する方法があります。

以下のファイルを作成しコードをファイルに記載して下さい。

touch src/asyncAwait.ts
touch test/asyncAwait.test.ts

[asyncAwait.ts]

export const timeoutWithMessage = (message: string, time: number) => {
  return new Promise((resolve, reject) => {
    if (time >= 0) {
      return setTimeout(() => resolve(message), time);
    } else {
      reject(new Error("timeの数値が不正です"));
    }
  });
}

[asyncAwait.test.ts]

import { timeoutWithMessage } from '../src/asyncAwait'

describe("timeoutWithMessage関数のテスト", () => {

  test('0.5秒後にmessageを返す', async () => {
    const message = 'test message';
    const time = 500; 
    const result = await timeoutWithMessage(message, time);
    expect(result).toBe(message);
});

  test('timeが不正の場合はエラーを返す', async () => {
    const message = 'test message';
    const time = -500;
    await expect(timeoutWithMessage(message, time)).rejects.toThrow("timeの数値が不正です");
});
})

因みにasync awaitを使わない場合は以下のようにpromiseチェーンでアサーションを実行する書き方になります。

  • async awaitで書く事がほぼほぼな気がしますが、promiseチェーンで書く必要などがある場合の参考にどうぞ。
test('asyncAwaitを使わずにテストする', () => {
  const message = 'test message';
  const time = 500; 
  return timeoutWithMessage(message, time).then(data => {
      expect(data).toBe(message);
  });
});

まとめ

最後まで読んでくださりありがとうございます。

以上がjestを使ったテストの基礎になります。

テスト戦略を踏まえた上で、テストを効果が大きい場所に効率良く書く事によって、より安全性が高いプロダクト開発が出来るのでは無いかと思います。

本当は外部要因(API, DBなど)を模倣してテストするMockテストまで含めて記事にしたかったのですが、記事の長さが倍になってしまうので、また次回にさせて頂きます。

この記事を読んで興味を持って下さった方がいらっしゃればカジュアルにお話させていただきたく、是非ご応募をお願いします! Wantedly / Green

次のアドベントカレンダーの記事はiimonが誇る「インフラの魔術師」ことhogeくんです。

どんなインフラの魔術的知識を共有してくれるのか楽しみです!

参考資料

https://zenn.dev/koki_tech/articles/a96e58695540a7 https://qiita.com/KNR109/items/7cf6b24bed318dab5715 https://rightcode.co.jp/blog/information-technology/testing-trophy-syain https://gihyo.jp/dev/serial/01/savanna-letter/0005 https://thinkit.co.jp/article/13346 https://jestjs.io/ https://jestjs.io/ja/docs/expect#matchers https://jestjs.io/ja/docs/expect