👌

JestのTips集10選。サーバーサイドでNode.jsのJestを書いたことない人向け

2022/04/11に公開3

対象

業務レベルでサーバーサイドでJestを書いたことはないけれど、新プロジェクトでは書くことになったみたいな方を想定して記述しています。

Jestについては中々ベストプラクティスが集まりにくいので、経験的にこう書くと「きれいに」・「早く」・「正確に」書けるよというTipsを集めてみました。もし、よろしければお読みください。

前提

  • TypeScript
  • Node.js
  • Jest
  • DBアクセスありの状態を想定しています

1. it文内では、必ず1回は、expectをつかって検証をする

JestのPRをレビューしてるとたまに見受けるのですが、expectを使ってないケースがあります。


// NG
it('userを正常に、作成できること', async() => {
  await createUser({ name: 'Mike' });
});

// OK
it('pdfが正常に削除できること', async() => {
  const user = await createUser({ name: 'Mike' });
  expect(user.name).toBe('Mike');
});

// OK
it('pdfが正常に削除できること', async() => {
  await createUser({ name: 'Mike' });
  
  const user = findOneUser({ name: 'Mike' });
  expect(user.name).toBe('Mike');
});


2. Objectの比較には、toStrictEqualを基本的に使う

詳細はかなりむずかしいのですが、脳死で、まずはtoStrictEqualを使いましょう。
厳密な比較になると覚えておけば問題ないです。

理由も交えて、より詳細に知りたい方はこちらが詳しいです。
https://zenn.dev/t_poyo/articles/4c47373e364718

type User = {
  userId: number;
  name: string;
};

// NG
expect(user).toEqual({ userId: 1, name: 'Mike' });

// OK
expect(user).toStrictEqual<User>({ userId: 1, name: 'Mike' });

3. expect.anyを使う時は、toEqualにする

createdAt, updatedAt, uuid形式のidの自動生成などはテストしにくいですが、expect.anyを使えば、簡単にかけます。その場合は、toStrictEqualで厳密な比較ができないので、toEqualにしましょう。

type User = {
  userId: number;
  name: string;
  createdAt: Date;
};

// OK
expect(user).toEqual({ userId: 1, name: 'Mike', createdAt: expect.any(Date) });

4. プリミティブな比較には、toBeを使う。

type User = {
  userId: number;
  name: string;
};

// NG
expect(user.userId).toStrictEqual(1);
expect(user.name).toStrictEqual('Mike');

// OK
expect(user.userId).toBe(1);
expect(user.name).toBe('Mike');

5. 外部ライブラリをテストする時は、基本的にはspyOnを使ってmock化する

もちろんspyOn以外の書き方もたくさんあるのですが、少し理解が難しいのと、import周りで並列実行時に悪さをすることがあるので、直感的にわかりやすいspyOn を使うのがオススメです。

例えば、以下のサンプルコードは、
fileというClassのdeleteFileFromS3というメソッドをmockします。その返り値はtrueです。といった感じでかなりわかりやすくかけると思います。

const deleteFileFromS3Mock = 
  jest.spyOn(file, 'deleteFileFromS3').mockResolvedValue(true);

そして、できれば、mock化しやすいようなfunctionの粒度にしちゃうとJestが書きやすいコードになります。
また、mockを使う時には、意図したmockがコールされたこと、意図してないmockがコールされてないことを確認するテストを書くことが重要です。.not.toHaveBeenCalled();``toHaveBeenCalled(); というmatcherがあるので利用しましょう。

mockは、使い終わったら、restoreして、元に戻してあげましょう。
mockをどこで定義してどのように使いまわしてるかによりますが、各it文内でmockをつくってあげて、it文の最後に元に戻すのであれば、restore()で十分です。

こちらが詳しいです。
https://qiita.com/yamagen0915/items/da885b3fa5cb825ccca9

import * as file from '/lib/file';

// some.ts
export const deletePdf = async (pdfId: number) => {
  await prisma.pdf.delete({ where: { id: pdfId } });
 const result = file.deleteFileFromS3(pdfId);
 return result
}

// some.test.ts
it('pdfが正常に削除できること', async() => {
  // fileのdeleteFileFromS3をSpyOnして、常にtrueをreturnさせる
  const deleteFileFromS3Mock = jest.spyOn(file, 'deleteFileFromS3').mockResolvedValue(true);
  
  const result = await deletePdf(pdfId);
  expect(result).toBe(true);
  
  // deletePdf内でfile.deleteFileFromS3のモックがコールされたことを確認する
  expect(deleteFileFromS3Mock).toHaveBeenCalled();
  // mockをrestoreする
  deleteFileFromS3Mock.restore();
});

6. 網羅したい系のテストでは、describe.eachやit.eachを使う

describeやitをコピペして使うのではなく、.each を使うときれいにかけるのでオススメです。

// some.ts
export const add = (a: number, b: number) => {
  return a + b; 
}

// some.test.ts
describe('test', () => {
  it.each([
    [1, 1, 2],
    [1, 2, 3],
    [2, 2, 4],
  ])("%i + %i = %i", (inputA, inputB, expected) => {
    expect(add(inputA, inputB)).toEqual(expectedResult)
  })
})

結果はこのように表示されます。

  test1 + 1 = 21 + 2 = 32 + 2 = 4

7. seedは必要なときに、必要な分を最低限つくる

DBアクセスを実際にしてる場合は、必要なタイミングで必要な分だけテスト用のseedデータをつくる。
IOを発生させればさせるほど、Jestの速度は遅くなります。

8. メールアドレスは、自社ドメインを使用すること

たまに、@hoge.jp, @test.comみたいなドメインを勝手につかってテストを書いてしまうケースがありますが、メールのご配信などのリスクがあるので、やめましょう。
自社、自分で獲得してるドメインのみを使いましょう。

参考:
https://blog.ko31.com/201304/sample-domain-example/

9. PromiseのErrorのテストには、expect().rejects.toThrow()を使う。

// some.ts
export const createUser = async(email: string) => {
  const user = findOneUser(email);
  if(user.email) {
   throw new Error('メールアドレスが重複しています。')
  }
  
  //... ユーザー作成処理
}

// some.test.ts
it('メールアドレスが重複してる場合エラーになること', () => {
  await expect(
    createUser({ email: 'sample@michibiku.co.jp' })
  ).rejects.toThrow('メールアドレスが重複しています。');
});

10. 大量レコードを作成して、境界値テストを書きたいときは、mockをつかって、DBに大量データがあるような状態にしてテストを書く

たとえば、以下の場合では、ユーザーがすでに101回以上作成されていれば、エラーが発生することのテストをしたい状況です。その場合に、愚直に101個ユーザーを実際に作成するのではなく、ユーザーをカウントしてるところをメソッド化して、それをspyOnにする。そして、そのmockはDBアクセス無しにただ101というnumberをreturnしてあげるようにすれば、Jestの速度が格別に早くなります。

// some.test.ts
it('ユーザーがすでに100人作成されてる時、エラーになること', () => {
  // mockで対応する。実際に101回createUserをコールしたりしない
  const findAllUsersMock = jest.spyOn(user, 'findAllUsers').mockResolvedValue(101);
  
  await expect(
    createUser({ email: 'sample@michibiku.co.jp' })
  ).rejects.toThrow('ユーザー作成の上限になりました。');
  
  // モックがコールされたことを確認する
  expect(findAllUsersMock).toHaveBeenCalled();
  // mockをrestoreする
  findAllUsersMock.restore();
 
});

Discussion

gifumastergifumaster
  1. メールアドレスは、自社ドメインを使用すること
    example.[com|net|org]、.test などを使いましょう。
    社内ドメインでもそこから外に出ていく可能性は十分にあります。
yuichkunyuichkun
1. it文内では、必ず1回は、expectをつかって検証をする

expect.hasAssertionsexpect.assertionsを使うとテスト内でassertionが行われていること(やその回数)をチェックすることができるので、それを必ず書くことをルール化しても良いかもしれませんね。

コーディング規約レベルでは不満足であれば、それを強制するeslintのルール もみつけたので、併用すればCIレベルでexpectのないテストを弾くことができるので良さそうです。

shingo.sasakishingo.sasaki

簡潔で素敵な記事をありがとうございます!
いくつか気になった点を質問させてください。

  1. it文内では、必ず1回は、expectをつかって検証をする

こちらは直感的には良さそうな感じがしますが、 expect を使わずに、例えば assert.equal を使っていた場合にどういう問題が起こり、 expect ならどのような恩恵があるんでしょうか?

  1. プリミティブな比較には、toBeを使う。

プリミティブな値について toEqual toStrictEqual を使った場合になにか不都合があるんでしょうか?