13
8

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.

jest.spyOnでTypeError: Cannot redefine property

Last updated at Posted at 2022-10-05

テスト書いてますか? 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では動かないという現象がありますが、これは別の記事にしようと思います。

13
8
1

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
13
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?