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

Windows正常运行的Jest Axios Mock在Mac上失效求助

Jest Axios Mock在Mac环境失效问题求助

我们团队此前仅使用Windows开发环境,近期引入Mac设备后,发现原本在Windows上正常工作的Jest Axios Mock逻辑完全失效——Mock被直接忽略,代码实际发起了对外部vCenter实例的REST请求。Windows环境中调试可看到流程进入mockGet的mockImplementation,但Mac环境中完全不执行Mock逻辑,因实际调用外部服务而失败。

相关Mock代码片段

const mockGet = jest.fn();
const mockPost = jest.fn();
const mockDelete = jest.fn();

// Mock the AxiosWrapper constructor
(AxiosWrapper as jest.MockedClass<typeof AxiosWrapper>).mockImplementation(() => ({
  get: mockGet,
  post: mockPost,
  delete: mockDelete,
} as unknown as AxiosWrapper));
...
it('example test', async () => {
  mockGet.mockImplementation<T = any>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> => {
    const baseResponse: AxiosResponse = {
      data: {},
      status: 200,
      statusText: 'OK',
      headers: {},
      config: {
        ...config,
        headers: config?.headers ?? {},
      } as InternalAxiosRequestConfig,
    };
    if (url.includes(createFileUrl)) {
      return Promise.resolve({ ...baseResponse, data: 'fileContent' } as AxiosResponse<T>);
    }
...

完整相关代码

AxiosWrapper.ts

import axios, { AxiosRequestConfig, AxiosResponse, Method } from 'axios';
import { JsonObject, ensureError } from 'Common-Utils';
import https from 'https';
import { logger } from './common/logger';
import { getEnv } from './index';

class AxiosWrapper {
  private apiHost: string;

  private sessionKey: string;

  private axiosConfig: AxiosRequestConfig;

  private vcenterUsername: string;

  private vcenterPassword: string;

  private httpsAgent: https.Agent;
  
  private axiosAuthConfig: object;

  private vcenterUsernamePasswordBase64: string;
  
  constructor(httpsAgent: https.Agent) {
    this.apiHost = getEnv('API_HOST');
    this.vcenterUsername = getEnv('VCENTER_USERNAME');
    this.vcenterPassword = getEnv('VCENTER_PASSWORD');
    this.httpsAgent = httpsAgent;
    this.vcenterUsernamePasswordBase64 = Buffer.from(`${this.vcenterUsername}:${this.vcenterPassword}`).toString('base64');
    this.sessionKey = '';
    this.axiosConfig = {
      httpsAgent: this.httpsAgent,
      headers: {
        'vmware-api-session-id': this.sessionKey,
      },
    };
    this.axiosAuthConfig = {
      httpsAgent: this.httpsAgent,
      headers: {
        Authorization: `Basic ${this.vcenterUsernamePasswordBase64}`,
      },
    };
  }

  async getSessionKey(): Promise<string> {
    let response;
    try {
      response = await axios.post(`https://${this.apiHost}/api/session`, {}, this.axiosAuthConfig);
    } catch (err) {
      const error = ensureError(err);
      logger.error(`Error getting sessionKey: ${error.message}`);
      throw new Error(`Error getting sessionKey: ${error.message}`, { cause: error });
    }
    return response.data;
  }

  async refreshSessionKey() {
    this.sessionKey = await this.getSessionKey();
    this.axiosConfig = {
      httpsAgent: this.httpsAgent,
      headers: {
        'vmware-api-session-id': this.sessionKey,
      },
    };
  }

  private async request<T>(
    method: Method,
    url: string,
    data?: JsonObject | string,
    config?: AxiosRequestConfig,
  ): Promise<AxiosResponse<T>> {
    const requestConfig: AxiosRequestConfig = {
      ...this.axiosConfig,
      ...config,
      method,
      url,
      data,
    };

    try {
      if (!this.sessionKey) {
        await this.refreshSessionKey();
        if (requestConfig.headers) {
          requestConfig.headers['vmware-api-session-id'] = this.sessionKey;
        }
      }
      logger.verbose(`Making ${method} request to ${url}`);
      const response: AxiosResponse<T> = await axios(requestConfig);
      return response;
    } catch (error) {
      logger.error(`Error making ${method} request to ${url}: ${error.message}`);
      if (method === 'DELETE' && url.includes('session') && error.response?.status === 401) {
        // this is not a big deal especially if the session key is expired
        logger.warn(`failed to delete session key: ${this.sessionKey}.  ${error.message}`);
      } else if (axios.isAxiosError(error) && error.response?.status === 401) {
        logger.warn('Session key expired. Refreshing session key');
        await this.refreshSessionKey();
        // Retry the request with the new session key
        requestConfig.headers = {
          ...requestConfig.headers,
          'vmware-api-session-id': this.sessionKey,
        };
        logger.verbose(`Retrying ${method} request to ${url}`);
        const retryResponse: AxiosResponse<T> = await axios(requestConfig);
        return retryResponse;
      }
      logger.error(`failed making ${method} request to ${url}: ${error.stack}`);
      throw error;
    }
  }

  async get<T>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
    return this.request<T>('GET', url, undefined, config);
  }

  async delete<T>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
    return this.request<T>('DELETE', url, undefined, config);
  }

  async post<T>(url: string, data?: JsonObject, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
    return this.request<T>('POST', url, data, config);
  }
}

export default AxiosWrapper;

vSphereClient.ts(调用示例)

public async getInternalVMName(name: string): Promise<string> {
  const response = await this.AxiosWrapper.get<JsonObject>(
    `https://${this.apiHost}/api/vcenter/vm?names=${name}`,
    this.axiosConfig,
  );

  if (!response?.data?.[0]?.vm) {
    throw new Error('Could not get internal VM name');
  }

  logger.verbose(`Internal VM Name: ${response?.data?.[0]?.vm}`);
  return response.data[0].vm;
}

vSphereClient.spec.ts(测试示例)

import path from 'path';
import { VSphereClient } from '../../lib/vSphereClient';
import AxiosWrapper from '../../lib/AxiosWrapper';

const sessionKey = 'session-key';
const apiHost = 'api-host.example.com';
const internalRegisteredVmName = 'vm-145';

jest.mock('../../lib/AxiosWrapper');
jest.mock('Common-Utils', () => ({
  sleep: jest.fn(),
}));
jest.useFakeTimers();

const mockExecuteCommand = jest.spyOn(require('../../lib/common/executeCmd'), 'executeCommand');

mockExecuteCommand.mockImplementation(() => ({ stdout: 'TEST_PASS', stderr: 'TEST_FAILED', error: 'ERROR' }));

// Create a mock implementation of AxiosWrapper
const mockAxiosWrapper = {
  get: jest.fn(),
  post: jest.fn(),
  delete: jest.fn(),
};

// @ts-ignore
jest.mocked(AxiosWrapper).mockImplementation(() => mockAxiosWrapper);

describe('VSphereClient', () => {
  afterEach(() => {
    jest.resetAllMocks();
  });

  it('getInternalVmName', async () => {
    mockAxiosWrapper.post.mockImplementation(async (url) => {
      if (url.includes('/api/session')) {
        return Promise.resolve(sessionKey);
      }
      return Promise.reject(new Error('Unexpected URL'));
    });
    mockAxiosWrapper.get.mockImplementation((url) => {
      if (url.includes(`vm?names=${internalRegisteredVmName}`)) {
        return Promise.resolve({});
      }
      return Promise.reject(new Error('Unexpected URL'));
    });

    const vSphereClient = await VSphereClient.Connection.create();
    await expect(vSphereClient.getInternalVMName(internalRegisteredVmName)).rejects.toThrowError('Could not get internal VM name');
  });
});

错误信息

2025-01-23 09:24:10 undefined error:  failed making GET request to https://api-host.example.com/api/vcenter/vm?names=vm-145: Error: getaddrinfo ENOTFOUND api-host.example.com
        at Function.Object.<anonymous>.AxiosError.from (/Users/jake/Projects/VMware-Utils/node_modules/axios/lib/core/AxiosError.js:89:14)
        at RedirectableRequest.handleRequestError (/Users/jake/Projects/VMware-Utils/node_modules/axios/lib/adapters/http.js:620:25)
        at RedirectableRequest.emit (node:events:519:28)
        at ClientRequest.eventHandlers.<computed> (/Users/jake/Projects/VMware-Utils/node_modules/follow-redirects/index.js:38:24)
        at ClientRequest.emit (node:events:519:28)
        at TLSSocket.socketErrorListener (node:_http_client:500:9)
        at TLSSocket.emit (node:events:519:28)
        at emitErrorNT (node:internal/streams/destroy:169:8)
        at emitErrorCloseNT (node:internal/streams/destroy:128:3)
        at processTicksAndRejections (node:internal/process/task_queues:82:21)
        at Axios.request (/Users/jake/Projects/VMware-Utils/node_modules/axios/lib/core/Axios.js:45:41)
        at processTicksAndRejections (node:internal/process/task_queues:95:5)
        at AxiosWrapper.request (/Users/jake/Projects/VMware-Utils/lib/AxiosWrapper.ts:89:42)

求各位提供解决思路,感谢。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 21:55:01