はじめに
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.
参考