テスト書いてますか? jest.spyOnするとき以下のように書いてないでしょうか?
import * as f from './functions'
jest.spyOn(f, "x").mockImplementation(() => 'x')
test('should be x', () => expect(f.x()).toBe('x'))
jsを書いているとクラスベースではなく関数群のmoduleを作ることが多く、またmock()書くのが面倒でついつい手を抜いてこのように書いてしまいますが、esm→cjsのファイルに対するjestのテストかつ組み合わせるトランスパイラによっては表題のエラーになります。
TL;DR
- babel-jest, tsc-jestを使っているなら大丈夫。babel/tscがesm準拠なcjsを吐くようになったらエラーになるはず。
- swc-jest, esbuild-jestを使う際はエラーになる。
- swc-jestの場合は jest_workaround のaddonを使う(exportするものをconfigurableにするやつ)。
- 別のテストフレームワークを使う(vitestは問題ない)。
- 素直にmock()を使う。
背景
nextjsのプロダクトを脱babelしてswcに移行する際、jestのテストもswc-jestに変更した際
TypeError: Cannot redefine property
のエラーに遭遇し、いろいろ調べてみました。
何が原因か
jestの実装と各トランスパイラの結果を見ればわかります。
esmで書いたものを
export const a = () => {}
をcjsに変換すると
babel, tsc(どっちも大体同じ)
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.a = void 0;
const a = () => { };
exports.a = a;
swc, esbuild(どっちも大体同じ)
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "a", {
enumerable: true,
get: function() {
return a;
}
});
var a = function() {};
jestの実装
let descriptor = Object.getOwnPropertyDescriptor(object, methodKey);
...
if (descriptor && descriptor.get) {
const originalGet = descriptor.get;
mock = this._makeComponent({type: 'function'}, () => {
descriptor!.get = originalGet;
Object.defineProperty(object, methodKey, descriptor!);
});
descriptor.get = () => mock;
Object.defineProperty(object, methodKey, descriptor);
} else {
mock = this._makeComponent({type: 'function'}, () => {
if (isMethodOwner) {
object[methodKey] = original;
} else {
delete object[methodKey];
}
});
// @ts-expect-error overriding original method with a Mock
object[methodKey] = mock;
}
descriptor.getがあるものつまりswc/esbuildでcjsにしたものになります。トランスパイラの結果はesmに準拠した形でconfigurableはデフォルト値のfalseなので、7行目にCannot redefined property errorになるわけです。
一方babel/tscはObject.definePropertyで定義するわけではなくそのままexportしているので、jestの実装の中でもelse側の処理で特にObject.definePropertyしていないのでエラーにはなりません。
じゃあどうするか
トランスパイラを変えるとエラーになるものだから、swc/esbuildには結構この手のissueがたくさんありました。ただ双方の言い分としては本来esmはimmutableなのでbabel/tscがおかしい、知らんという話でした。以下コメントの中でswcのコミッターの人がesmをmutableにするaddonを作ってくれていますが非推奨としています。
素直にmockを使って書くのが良さそうです。あるいは別のテストフレームワーク、チームでは一部vitestを使っていてトランスパイラはesbuildですが、こちらはspyOnの実装が異なるのでこのエラーにはならないようです。
vitestでもspyOnの実装がjestと異なるので、jestでは動いていたのにvitestでは動かないという現象がありますが、これは別の記事にしようと思います。