You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用TS-Jest正确模拟child_process的spawn命名导入?

如何用TS-Jest正确模拟ESM命名导入的child_process.spawn?

我尝试模拟从child_process命名导入的spawn,但该属性为只读,导致模拟失败。进一步发现child_process.spawn和import { spawn } from "child_process"并非同一引用,目前只能临时改用默认导入,但大型项目中这种修改方案不现实。请问是否存在使用TS-Jest模拟ESM命名导入的正确方法?

相关代码

src/factory.ts

import { spawn } from "child_process";

class ComposeFactory implements ContainerFactory {
  constructor(type: string) {
    this.type = type;
  }
  async create(name: string) {
    console.log(`[${this.type} ${name}]: Creating.`);
    await spawn(
        "/usr/bin/docker",
        [
          "compose",
          "-f",
          `${process.env.DOCKER_COMPOSE_PATH}/docker-compose.${this.type}.yaml`,
          "create",
        ],
        {
          stdio: "ignore",
          detached: true,
        }
      )
      .unref();

    this.containerInstances.set(name, new ComposeInstance(this.type, name));
  }
  // ...(delete函数逻辑类似)
}

test/factory.test.ts(原失败的模拟代码)

import { describe, expect, test, jest } from "@jest/globals";
import child_process, { ChildProcess } from "child_process";

import ComposeFactory from "../src/factory";

const spawn = jest
  .spyOn(child_process, "spawn")
  .mockImplementation(function (
    this: ChildProcess,
    command: string,
    args: readonly string[],
    options: child_process.SpawnOptions
  ): ChildProcess {
    this.unref = jest.fn();
    console.log("Correctly mocked spawn");
    return this;
  });

解决方案

方法1:使用jest.mock直接模拟模块的命名导出

这种方法是最规范的,直接替换整个模块的spawn导出,能覆盖ESM命名导入的引用:

// test/factory.test.ts
import { describe, expect, test, jest } from "@jest/globals";
import { spawn } from "child_process";
import ComposeFactory from "../src/factory";

// 模拟child_process模块,保留其他原有导出,仅替换spawn
jest.mock("child_process", () => ({
  ...jest.requireActual("child_process"), // 保留未被模拟的其他API
  spawn: jest.fn(function(this: any, command: string, args: readonly string[], options: any) {
    this.unref = jest.fn();
    console.log("Correctly mocked spawn");
    return this;
  })
}));

describe("ComposeFactory", () => {
  test("create方法应正确调用spawn", async () => {
    const factory = new ComposeFactory("test");
    await factory.create("test-container");
    
    // 验证spawn的调用参数
    expect(spawn).toHaveBeenCalledWith(
      "/usr/bin/docker",
      [
        "compose",
        "-f",
        `${process.env.DOCKER_COMPOSE_PATH}/docker-compose.test.yaml`,
        "create"
      ],
      { stdio: "ignore", detached: true }
    );
    
    // 验证unref方法被调用
    const mockSpawnResult = spawn.mock.results[0].value;
    expect(mockSpawnResult.unref).toHaveBeenCalled();
  });
});

方法2:通过Object.defineProperty绕过只读限制(不推荐,仅作备选)

如果不想模拟整个模块,可以通过重新定义属性的方式绕过只读限制,再进行spy:

import { describe, expect, test, jest } from "@jest/globals";
import * as child_process from "child_process";
import ComposeFactory from "../src/factory";

describe("ComposeFactory", () => {
  let spawnSpy: jest.SpyInstance;
  
  beforeEach(() => {
    // 重新定义spawn属性,设置为可写
    Object.defineProperty(child_process, "spawn", {
      writable: true,
      value: jest.fn(function(this: any) {
        this.unref = jest.fn();
        console.log("Correctly mocked spawn");
        return this;
      })
    });
    spawnSpy = child_process.spawn as jest.SpyInstance;
  });

  afterEach(() => {
    // 测试后恢复所有mock
    jest.restoreAllMocks();
  });

  test("create方法应触发mock的spawn", async () => {
    const factory = new ComposeFactory("test");
    await factory.create("test-container");
    
    expect(spawnSpy).toHaveBeenCalled();
  });
});

问题原因

ESM的命名导入和模块对象(import * as child_process)上的属性在TS转译或Node.js的ESM处理机制下,可能并非同一引用。因此直接jest.spyOn(child_process, "spawn")无法影响命名导入的spawn,而jest.mock是直接替换模块的导出内容,能确保命名导入的是mock后的函数。

内容的提问来源于stack exchange,提问作者ChrisAB

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 23:52:09