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

Angular迁移Jest测试报toHaveBeenCalledOnceWith类型不存在

问题描述

项目新增3个JSON文件作为动态配置,相关配置会在应用初始化阶段加载。将原有Jasmine-Karma测试代码迁移至Jest后,运行测试时出现如下类型报错:

Property 'toHaveBeenCalledOnceWith' does not exist on type 'JestMatchers<Mock<any, any>>'.

关联项目代码

app.module.ts 配置

export function configLoader(injector: Injector) : () => Promise<any>
{
    return () => injector.get(ConfigurationService).loadConfiguration();
}
export function configProdLoader(injector: Injector) : () => Promise<any> {
    return () => injector.get(ConfigurationService).loadProdConfig();
}

export function configEnvironmentLoader(injector: Injector) : () => Promise<any>
{
    return () => injector.get(ConfigurationService).loadEnvironmentConfig();
}

App模块的providers配置如下:

{provide: APP_INITIALIZER, useFactory: configLoader, deps: [Injector], multi: true},
{provide: APP_INITIALIZER, useFactory: configProdLoader, deps: [Injector], multi: true},
{provide: APP_INITIALIZER, useFactory: configEnvironmentLoader, deps: [Injector], multi: true},

test.spec.ts 测试代码

describe("ConfigurationService", () => {

    const returnValue = {};

    let httpMock: {get: jest.Mock};

    let service: ConfigurationService;

    beforeEach(() => {
        httpMock = {
            get: jest.fn(() => of(returnValue)),
        };

        service = new ConfigurationService(<any>httpMock);
    });

    test('Should call the endpoint and retrieve the config', (done) => {
        service.loadConfiguration().then(() => {
            expect(httpMock.get)
                .toHaveBeenCalledOnceWith(service['configPath']);
            expect(service['configData']).toBe(returnValue);
            done();
        });
    });

    test('Should call the endpoint and retrieve the configProd', (done) => {
        service.loadProdConfig().then(() => {
            expect(httpMock.get)
                .toHaveBeenCalledOnceWith(service['configProdPath']);
            expect(service['configProdData']).toBe(returnValue);
            done();
        });
    });

    test('Should call the endpoint and retrieve the configEnvironment', (done) => {
        service.loadEnvironmentConfig().then(() => {
            expect(httpMock.get)
                .toHaveBeenCalledOnceWith(service['configEnvironmentPath']);
            expect(service['configEnvironmentData']).toBe(returnValue);
            done();
        });
    });
});

service.ts 服务实现代码

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class ConfigurationService {
    private configData: any | undefined;
    private configProdData: any | undefined;
    private configEnvironmentData: any | undefined;
    private readonly configPath: string = '../../assets/config/aws-config.json';
    private readonly configProdPath: string = '../../assets/config/prod-config.json';
    private readonly configEnvironmentPath: string = '../../assets/config/environment-config.json';

  constructor(private httpClient: HttpClient) { }

    async loadConfiguration(): Promise<any> {
        try {
            const response = await this.httpClient.get(`${this.configPath}`)
                .toPromise().then(res => this.configData = res);
            return this.configData;
        } catch (err) {
            return Promise.reject(err);
        }
    }

    get config(): any | undefined {
        return this.configData;
    }

    async loadProdConfig(): Promise<any> {
        try {
            const response = await this.httpClient.get(`${this.configProdPath}`)
                .toPromise().then(res => this.configProdData = res);
            return this.configProdData;
        } catch (err) {
            return Promise.reject(err);
        }
    }

    get configProd(): any | undefined {
        return this.configProdData;
    }

    async loadEnvironmentConfig(): Promise<any> {
        try {
            const response = await this.httpClient.get(`${this.configEnvironmentPath}`)
                .toPromise().then(res => this.configEnvironmentData = res);
            return this.configEnvironmentData;
        } catch (err) {
            return Promise.reject(err);
        }
    }

    get configEnvironmentProd(): any | undefined {
        return this.configEnvironmentData;
    }
}
问题根因
  • 核心报错原因:toHaveBeenCalledOnceWith是Jasmine测试框架专属的内置匹配器,Jest原生没有提供这个API,从Karma+Jasmine技术栈迁移到Jest后直接调用该方法,就会触发类型不存在的报错。
  • 测试代码隐性缺陷:三个测试用例复用同一个httpMock.get的mock实例,没有在用例执行前重置mock状态,不同用例的调用记录会互相污染,会导致断言结果不符合预期。
修复方案

方案1:替换为Jest原生断言(无额外依赖,推荐)

用两个Jest原生断言组合实现和toHaveBeenCalledOnceWith完全一致的校验逻辑:

  • 用toHaveBeenCalledTimes(1)校验mock方法仅被调用1次
  • 用toHaveBeenCalledWith(目标参数)校验mock方法入参符合预期
    同时在beforeEach钩子中增加mock重置逻辑,避免用例间状态污染。
    修改后的测试代码核心片段:
beforeEach(() => {
    httpMock = {
        get: jest.fn(() => of(returnValue)),
    };
    // 每个用例执行前清空所有mock的调用记录、缓存状态
    jest.clearAllMocks();
    service = new ConfigurationService(<any>httpMock);
});

test('Should call the endpoint and retrieve the config', (done) => {
    service.loadConfiguration().then(() => {
        // 替换原有的toHaveBeenCalledOnceWith断言
        expect(httpMock.get).toHaveBeenCalledTimes(1);
        expect(httpMock.get).toHaveBeenCalledWith(service['configPath']);
        expect(service['configData']).toBe(returnValue);
        done();
    });
});

// 其余两个测试用例按照相同规则替换断言即可

方案2:引入Jest兼容插件(保留原有断言写法)

如果不想修改已有的断言逻辑,可以安装jest-jasmine2运行器,或在Jest setup文件中手动扩展匹配器,让Jest兼容toHaveBeenCalledOnceWithAPI。该方案会增加项目依赖,维护成本更高,不作为优先选择。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 03:39:34