PAYATHON 2026

如何只在某个 Angular 单元测试中更新 signal 值?

数据阿明

每个测试都应创建独立的 WritableSignal,让服务和组件引用同一个 signal,并在需要时通过 .set() 修改它的值。断言时要调用 signal:component.mySignal().extraProperty。

为什么替换 getter 没有效果?

组件中的这行代码在初始化时读取服务属性,并保存返回的 signal 引用:

mySignal: Signal<any> = this.myService.mySignal;

组件创建后,即使让服务 getter 返回另一个 signal,也不会改变组件已经保存的引用。fixture.detectChanges() 同样不会重新执行这条字段初始化语句。

原来的 getter 每次执行时还会创建一个新的 signal:

get: () => signal(mockSignalValue)

因此,不同的读取者可能拿到不同的实例。测试时应先创建 signal,再让服务始终返回这个实例。

为每个测试创建可修改的 signal

下面沿用原例中把 MyComponent 放入 imports 的方式,前提是它是 standalone 组件。示例假设组件只需要服务的 mySignal;如果组件还访问其他成员,也需要在 mock 中提供。

import { signal, WritableSignal } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyComponent } from './my-component';
import { MyService } from './my-service';

interface SignalValue {
  someProperty: string;
  extraProperty?: string;
}

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

  beforeEach(async () => {
    testSignal = signal<SignalValue>({ someProperty: '' });

    const myServiceMock = {
      mySignal: testSignal,
    };

    await TestBed.configureTestingModule({
      imports: [MyComponent],
      providers: [
        { provide: MyService, useValue: myServiceMock },
      ],
    }).compileComponents();

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

  it('should not have the extra property', () => {
    expect(component.mySignal().extraProperty).toBeUndefined();
  });

  it('should have the extra property', () => {
    testSignal.set({
      someProperty: '',
      extraProperty: '',
    });

    fixture.detectChanges();

    expect(component.mySignal().extraProperty).toBe('');
  });
});

beforeEach 每次都会创建新的 signal 和初始对象,所以第二个测试中的 .set() 不会影响其他测试。组件可以将它声明为只读的 Signal,测试仍可通过保留的 WritableSignal 引用修改其值。

如果需要保留 createSpyFromClass,也可以将服务属性配置为返回同一个实例:

Object.defineProperty(myServiceSpy, 'mySignal', {
  get: () => testSignal,
  configurable: true,
});

这段配置应放在组件创建之前,之后通过 testSignal.set(...) 修改值即可。

修正组件中的服务注入

原例中的 constructor(myService: MyService) {} 只声明了构造函数参数,没有创建 this.myService 成员。可以在构造函数中保存 signal:

export class MyComponent {
  mySignal: Signal<any>;

  constructor(myService: MyService) {
    this.mySignal = myService.mySignal;
  }
}

Signal 类型应从 @angular/core 导入。实际项目中可以用业务类型替换 any。

初始化时机与断言注意事项

  • 如果只检查 signal 的当前值,.set() 后可以直接读取;如果检查模板更新,再调用 fixture.detectChanges()。
  • 如果测试构造函数或 ngOnInit 根据初始值执行的逻辑,应在 TestBed.createComponent() 之前设置所需值。可以将组件创建移到各个测试中,并在第一次 fixture.detectChanges() 前完成设置。
  • extraProperty 的值是空字符串 '',不能用真假值判断它是否存在。若要严格检查对象是否拥有该属性,可以使用下面的断言:
expect(
  Object.prototype.hasOwnProperty.call(
    component.mySignal(),
    'extraProperty',
  ),
).toBe(true);

原始配置还有两处语法问题:imports: [MyComponent]; 后应使用逗号,compileComponents 应写成方法调用 .compileComponents()。测试末尾也需要补齐 });。

备注:内容仅供参考。