💯

JestでOptionalな値をテストする

2024/02/12に公開

経緯

e2eテストのコードを書いているときに、以下のようなオブジェクトの型が正しいかテストしたい場面がありました。

{
    name: string,
    email: string,
    birthday?: string,
}

birthdayが存在する場合、以下のようにテストを作成することができます。

const test = {
  name: 'test',
  email: 'example@example.com',
  birthday: '2024-02-12',
}

expect(test).toEqual({
  name: expect.any(String),
  email: expect.any(String),
  birthday: expect.any(String),
});

ではbirthdayがundefinedの場合はどのようにテストするのでしょうか?
実はJestでoptionalな値をテストするメソッドはありません

ではどうするか

カスタムマッチャーを作成します。
カスタムマッチャーを使うと、自分でexpect.hogehoge(huga)のようなマッチャーを作成しテストすることができます。
詳しくは公式のページを見てください。
公式ページ: https://jestjs.io/docs/expect#custom-matchers-api

今回はOptionalな値をテストするカスタムマッチャーを作っていきます。

作成したカスタムマッチャー

export const equalOrUndefined = (received: any, expected: any) => {
  if (received === undefined) {
    return {
      message: () => `expected ${received} to be undefined`,
      pass: true,
    };
  }

  try {
    expect(received).toEqual(expected);
  } catch (e: any) {
    return {
      message: () => e?.message ?? `expected ${received} to be ${expected}`,
      pass: e?.pass ?? false,
    };
  }

  return {
    message: () => `expected ${received} to be ${expected}`,
    pass: true,
  };
};

expect(x).equalOrUndefined(y)の場合、receivedにx, expectedがyという対応になります。

やってることは単純でundefinedの場合はテストを通して、それ以外の場合はtoEqualで値が正しいかをチェックしています。

このカスタムマッチャーをexpect.extendを使って登録することで使うことができるようになります。

jest.d.ts
declare global {
  namespace jest {
    interface Matchers<R> {
      equalOrUndefined(expected: any): R;
    }
    interface Expect {
      equalOrUndefined(expected: any): any;
    }

    interface InverseAsymmetricMatchers {
      equalOrUndefined(received: any, expected: any): any;
    }
  }
}

export {};

実際に実行する

以下のテストを実行します。

expect.extend({
  equalOrUndefined,
});

describe('equalOrUndefinedのテスト', () => {
  it('正しい文字列でテストが通るか', () => {
    const test1: string | undefined = 'test';
    expect(test1).equalOrUndefined('test');
  });

  it('undefinedでテストが通るか', () => {
    const test2: string | undefined = undefined;
    expect(test2).equalOrUndefined('test');
  });

  it('違う文字列でテストが失敗するか', () => {
    const test3: string | undefined = 'Test';
    expect(test3).not.equalOrUndefined('test');
  });
});

実行結果

実行結果
equalOrUndefinedのテスト
    ✓ 正しい文字列でテストが通るか (1 ms)
    ✓ undefinedでテストが通るか
    ✓ 違う文字列でテストが失敗するか (1 ms)

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

無事テストが通りました。

最初のオブジェクトのテストを実行

最初のオブジェクトはカスタムマッチャーを使って以下のようにテストすることができます。

const test = {
  name: 'test',
  email: 'example@example.com',
  birthday: '2024-02-12',
}

expect(test).toEqual({
  name: expect.any(String),
  email: expect.any(String),
  birthday: expect.equalOrUndefined(
    expect.any(String),
  ),
});

以下のテストを実行してみます。

describe('Objectでのテスト', () => {
  it('文字列でテストが通るか', () => {
    const test = {
      name: 'test',
      email: 'example@example.com',
      birthday: '2024-02-12',
    };

    expect(test).toEqual({
      name: expect.any(String),
      email: expect.any(String),
      birthday: expect.equalOrUndefined(expect.any(String)),
    });
  });

  it('undefinedでテストが通るか', () => {
    const test = {
      name: 'test',
      email: 'example@example.com',
    };

    expect(test).toEqual({
      name: expect.any(String),
      email: expect.any(String),
      birthday: expect.equalOrUndefined(expect.any(String)),
    });
  });
});
実行結果
Objectでのテスト
    ✓ 文字列でテストが通るか (1 ms)
    ✓ undefinedでテストが通るか

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        0.825 s, estimated 1 s

正しくテストができることが確認できました。

Discussion