jest.spyOn を呼び出すと TypeError: Cannot redefine property が発生する
はじめに
そもそもは Microsoft Graph JavaScript Client Library (@microsoft/microsoft-graph-client
) を 3.0.0 に上げたときに BatchResponseContent
をモックするテストが失敗するようになったところから始まります。モックしようとしている BatchResponseContent
のコードには何も変更はありません。ちょっとよくわからなかったので issue を上げてみました。
結論をいうと「3.0.0 から TypeScript 4 でビルドするようになったのでその影響ですよ」とのこと。なるほどとは思いながらも理解のために少し現象を追ってみました。
サンプル コード
実行手順
中身は同じコードで TypeScript のバージョンだけが違う 2 つのプロジェクトを用意します。
src/HelloWorld.ts
export class HelloWorld {
output() {
console.log('Hello World');
}
}
src/index.ts
export * from './HelloWorld';
tsconfig.json
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"declaration": true,
"outDir": "dist"
},
"include": [
"src"
]
}
テスト コード
上記で作成したプロジェクトを参照して jest.spyOn
でモックします。
import * as typescript2 from 'typescript2';
import * as typescript4 from 'typescript4';
it('jest.spyOn() with typescript 2', () => {
jest.spyOn(typescript2, 'HelloWorld');
});
it('jest.spyOn() with typescript 4', () => {
jest.spyOn(typescript4, 'HelloWorld');
});
これを実行すると以下の結果になります。事象としては再現できていますね。TypeSctipt 2 ではテストが成功していますが TypeScript 4 ではテストが失敗します。
FAIL src/index.test.ts
√ jest.spyOn() with typescript 2 (1 ms)
× jest.spyOn() with typescript 4 (1 ms)
● jest.spyOn() with typescript 4
TypeError: Cannot redefine property: HelloWorld
at Function.defineProperty (<anonymous>)
7 |
8 | it('jest.spyOn() with typescript 4', () => {
> 9 | jest.spyOn(typescript4, 'HelloWorld');
| ^
10 | });
11 |
at ModuleMocker.spyOn (node_modules/jest-mock/build/index.js:831:16)
at Object.<anonymous> (src/index.test.ts:9:8)
ビルドされたコード
dist/index.js
TypeScript 2.9.2 でビルドしたコードが以下の通りです。
"use strict";
function __export(m) {
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
}
Object.defineProperty(exports, "__esModule", { value: true });
__export(require("./HelloWorld"));
TypeScript 4.4.3 でビルドしたコードが以下の通りです。
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
__exportStar(require("./HelloWorld"), exports);
明らかに違うよねというのは、__exportStar
からそれぞれのプロパティに対して __createBinding
を呼んでいて、最終的に Object.defineProperty
が呼ばれるということ。ここで既定値の configurable: false
が指定されて読み取り専用としてマークされてしまうことが、jest.spyOn
でエラーになる根本原因と思われます。
おわりに
じゃあどうすればいいのよって話になるのですがいくつかのオプションがあります。
インポート先のファイルを index.js から個々のファイルに変更する
たとえば先のサンプルの場合は以下のようにすることで回避が可能です。__exportStar
が呼ばれないので jest.spyOn
が可能です。
import * as typescript2 from 'typescript2';
- import * as typescript4 from 'typescript4';
+ import * as typescript4 from 'typescript4/dist/HelloWorld';
it('jest.spyOn() with typescript 2', () => {
jest.spyOn(typescript2, 'HelloWorld');
});
it('jest.spyOn() with typescript 4', () => {
jest.spyOn(typescript4, 'HelloWorld');
});
ただしライブラリが webpack してる場合はこの方法はできないです。
ライブラリ側で export * をやめる
export するプロパティを指定してあげると __exportStar
が生成されないので回避できるようです。
- export * from './HelloWorld';
+ export { HelloWorld } from './HelloWorld';
ただしライブラリを変更しないといけないので難しくはあります。
jest.spyOn をあきらめる
おとなしく jest.mock
を使いましょう。
Discussion