0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[Node.js][TypeScript] jestで独自定義の例外クラスがthrowされることを確認したい時の落とし穴

Last updated at Posted at 2022-12-13

はじめに

jestで何かしらがthrowされる時は、公式通りにtoThrow()マッチャを使えば良いと思っていた時期が僕にもありました。

バージョンなど

macOS 12.6
node v18.12.1
npm 8.19.2
"@types/jest": "^28.1.8",
"jest": "^28.1.3",
"ts-jest": "^28.0.8",
"typescript": "^4.9.3"

事象

デバッグしても実行ログを見てもthrowされているのに、
Received function did not throwが表示されてテストがこける

 FAIL   sample/__tests__/index.spec.ts
  ✕ should thrown in doSomething (1 ms)

  ● should thrown in doSomething

    expect(received).rejects.toThrow(expected)
    Expected constructor: HandmadeException
    Received function did not throw

       8 |
       9 |   // HandmadeExceptionクラスがthrowされることを期待
    > 10 |   await expect(app.doSomething()).rejects.toThrow(HandmadeException)
         |                                           ^
      11 | })
      12 |

      at Object.toThrow (node_modules/expect/build/index.js:241:22)
      at Object.<anonymous> (sample/__tests__/index.spec.ts:10:43)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        0.881 s, estimated 1 s
Ran all test suites matching /index.spec.ts/i.

テストしようとしていたもの

こんな感じでテストしたい処理がある。
HandmadeExceptionはお手製のExceptionクラス。

sample/index.ts
class App {
  async doSomething(): Promise<void> {
    try {
      await someModule.sleep()
    } catch (error) {
      throw new HandmadeException('failed to make dir', error as string)
    }
  }
}

const someModule = {
  sleep: () => {
    return new Promise((resolve) => setTimeout(resolve, 1000))
  },
}

class HandmadeException {
  msg: string
  stack?: string | undefined
  constructor(msg: string, stack?: string) {
    this.msg = msg
    this.stack = stack
  }
}

export { App, someModule, HandmadeException }
sample/__tests__/index.spec.ts
import { App, someModule, HandmadeException } from '..'

const app = new App()

it('should thrown in doSomething', async () => {
  // someModule.sleep()でrejectされるようmock
  jest.spyOn(someModule, 'sleep').mockRejectedValueOnce('error!')

  // HandmadeExceptionクラスがthrowされることを期待
  await expect(app.doSomething()).rejects.toThrow(HandmadeException)
})

原因

jestのtoThrow(error?)マッチャの実装コードを見てみると、
throwされたobjectのmessage === 期待するobjectのmessageを検証していました。

const pass = thrown !== null && thrown.message === expected.message;

なので、プロパティとしてmessageを持つErrorコンストラクタがthrowされている場合や、
Errorをextendsして作成された独自定義の例外クラスならば、toThrow(error?)マッチャが使えた、ということですね。
implementsでもいいかもと思ったけど、テストが通らなかった

対策

その1 独自定義例外クラスは必ずErrorを継承する

- class HandmadeException {
+ class HandmadeException extends Error {
    msg: string
    stack?: string | undefined
    constructor(msg: string, stack?: string) {
+       super()
        this.msg = msg
        this.stack = stack
    }
}

その2 テスト時にtoThrow(error?)マッチャ以外を使用する

自分は.toBeInstanceOf(Class)を使いました。

// sample/__tests__/index.spec.ts
it('should thrown in doSomething', async () => {
  // someModule.sleep()でrejectされるようmock
  jest.spyOn(someModule, 'sleep').mockRejectedValueOnce('error!')

  // HandmadeExceptionクラスがthrowされることを期待
-  await expect(app.doSomething()).rejects.toThrow(HandmadeException)
+  await expect(app.doSomething()).rejects.toBeInstanceOf(HandmadeException)
})

結果

 PASS   sample/__tests__/index.spec.ts
  ✓ should thrown in doSomething (2 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.946 s, estimated 1 s
Ran all test suites matching /index.spec.ts/i.

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?