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

Angular单元测试中Mock/Spy辅助函数遇可写性报错问题

解决Angular单元测试中spyOn辅助函数的可写性报错问题

问题原因

通过import * as Helpers from 'helper-lib'导入的模块对象是只读的,Jasmine的spyOn需要修改对象属性来替换为间谍函数,但只读属性不允许被修改,因此抛出fn1 is not declared writable or has no setter错误。

解决方案

方案1:将辅助函数封装为Angular服务(推荐)

契合Angular依赖注入的设计思路,便于测试和长期维护:

  1. 重构辅助模块为Angular服务
// helper.service.ts
import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class HelperService {
  fn1(a: number, b: number): number {
    return a * b;
  }

  fn2(a: number, b: number): number {
    return a + b;
  }

  fn3(a: number, b: number): number {
    return a - b;
  }
}
  1. 修改组件,通过依赖注入使用服务
// myComponent.ts
import { Component } from '@angular/core';
import { HelperService } from 'helper-lib';

@Component({...})
export class MyComponent {
  constructor(private helperService: HelperService) {}

  public btnClick(p1: number, p2: number): number {
    let value = this.helperService.fn1(p1, p2);
    console.log(value);
    return value;
  }
}
  1. 单元测试中spyOn服务方法
// myComponent.spec.ts
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { MyComponent } from './myComponent';
import { HelperService } from 'helper-lib';

describe('MyComponent', () => {
  let component: MyComponent;
  let fixture: ComponentFixture<MyComponent>;
  let helperService: jasmine.SpyObj<HelperService>;

  beforeEach(waitForAsync(() => {
    // 创建服务的间谍对象
    const spy = jasmine.createSpyObj('HelperService', ['fn1']);

    TestBed.configureTestingModule({
      declarations: [MyComponent],
      providers: [{ provide: HelperService, useValue: spy }]
    }).compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(MyComponent);
    component = fixture.componentInstance;
    helperService = TestBed.inject(HelperService) as jasmine.SpyObj<HelperService>;
    fixture.detectChanges();
  });

  it('should calc value', () => {
    helperService.fn1.and.returnValue(2); // 模拟返回值

    let value = component.btnClick(1, 2);

    expect(helperService.fn1).toHaveBeenCalledWith(1, 2);
    expect(value).toBe(2);
  });
});

方案2:修改模块导出方式,用可写对象包裹辅助函数

如果不想重构为服务,可调整模块导出结构,让函数位于可写对象中:

  1. 重构myHelpers.ts的导出
// myHelpers.ts
export const Helpers = {
  fn1(a: number, b: number): number {
    return a * b;
  },
  fn2(a: number, b: number): number {
    return a + b;
  },
  fn3(a: number, b: number): number {
    return a - b;
  }
};
  1. 组件保持原有导入方式,测试时直接spyOn
// myComponent.spec.ts
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { MyComponent } from './myComponent';
import { Helpers } from 'helper-lib';

describe('MyComponent', () => {
  let component: MyComponent;
  let fixture: ComponentFixture<MyComponent>;

  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({declarations:[MyComponent]}).compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(MyComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should calc value', () => {
    const mySpy = spyOn(Helpers, 'fn1').and.returnValue(2);

    let value = component.btnClick(1, 2);

    expect(mySpy).toHaveBeenCalledWith(1, 2);
    expect(value).toBe(2);
  });
});

方案3:临时修改属性可写性(不推荐,hack方式)

若无法修改辅助模块结构,可在测试中临时将函数属性改为可写:

// myComponent.spec.ts
it('should calc value', () => {
  // 临时将fn1改为可写
  Object.defineProperty(Helpers, 'fn1', { writable: true });
  const mySpy = spyOn(Helpers, 'fn1').and.returnValue(2);

  let value = component.btnClick(1, 2);

  expect(mySpy).toHaveBeenCalledWith(1, 2);
  expect(value).toBe(2);

  // 测试后恢复只读(可选)
  Object.defineProperty(Helpers, 'fn1', { writable: false });
});

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 19:55:19