Angular 17+ testing services with signals and effects

I am not very experienced with Angular signals, especially with services with signals and effects.

Basically, I have a service A which exposes a public method that sets/updates a private signal in the service. Anytime the value of the signal in service A changes, it triggers an effect (called in the constructor of the service), which effect calls a private method of service A. The private method is used to call a number of other different services’ methods, but for the sake of simplicity let’s just say that it’s only one service – service B, and a exposed method of service B.

The code works as it’s supposed to, but I need to write tests for this system and it seems like I cannot wrap my head around how services with signals, especially the triggered effects.

The goal of the test is to verify that once public method of service A (which updates the signal) is called, that also the whole chain happens, i.e. eventually public method of service B is called.

I’ve tried a number of different solutions, including using fakeAsunc + tick, TestBed.flushEffects, runInInjectionContext, and many other hacky solutions that defeat the purpose of writing tests.

Example:

@Injectable({
  providedIn: 'root'
})
export class ServiceA {
  private signalA: Signal<number> = signal(0);

  constructor(private readonly serviceB: ServiceB) {
    effect(() => {
      const signalAValue = signalA(); 
      this.privateMethod(signalAValue);
    });
  }

  public publicMethodA(value: number): void {
    this.signalA.update(value);
  }

  private privateMethodA(arg: number): void {
    this.serviceB.publicMethodB(arg)
  }
}

Test for ServiceA:

describe('ServiceA', () => {
  let serviceA: ServiceA;
  let serviceB: ServiceB;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        ServiceA,
        ServiceB
      ]
    });

    serviceA = TestBed.inject(ServiceA);
    serviceB = TestBed.inject(ServiceB);
  });

  it('should call publicMethodB of ServiceB when publicMethodA of ServiceA is called', fakeAsync(() => {
    jest.spyOn(serviceA, 'publicMethodA');
    service.publicMethod(1);

    expect(serviceB.publicMethodB).toHaveBeenCalled();
  }));
});

Test fails with:

   Expected number of calls: >= 1
    Received number of calls:    0

Effect don’t flush automatically. You need to do it yourself with TestBed.flushEffect.

it('should call publicMethodB of ServiceB when publicMethodA of ServiceA is called', fakeAsync(() => {
    jest.spyOn(serviceB, 'publicMethodB');
    service.publicMethod(1);
    TestBed.flushEffect(); // You need to manually flush the effects 
    expect(serviceB.publicMethodB).toHaveBeenCalled();
}));

2

The problem is that you added the services to the providers of the Test Bed testing module. As a consequence, the instance of ServiceB injected by ServiceA is a different one than the instance you are spying on. Just remove the services from the providers and it should work.

Further reading: https://angular.dev/guide/di/dependency-injection

1

Since you spied on publicMethodA the method is never called, because the spy stops the execution of the actual method. I think you need to spy on method publicMethodB of service B instead.

it('should call publicMethodB of ServiceB when publicMethodA of ServiceA is called', fakeAsync(() => {
    jest.spyOn(serviceB, 'publicMethodB');
    service.publicMethod(1);
    flush();
    expect(serviceB.publicMethodB).toHaveBeenCalled();
}));

2

The reason why you can’t trigger the update of your signal is connected to the ability to execute the effect.


Angular 17 and after:

Post Angular 17, you can use the function TestBed.flushEffect()s, like so:

it('should call publicMethodB of ServiceB when publicMethodA of   ServiceA is called', fakeAsync(() => {
    jest.spyOn(serviceB, 'publicMethodB');
    service.publicMethod(1);

    TestBed.flushEffects(); // <- This!
    expect(serviceB.publicMethodB).toHaveBeenCalled();
}));

Note: in Angular 17 and 18, the function is considered developer preview, it is possible further changes to it are done.

Angular 16 only:

The function does not exist, so we need to find another way to do trigger the effect. Looking at the official DOC for clues, we find the following for components:

When the value of that signal changes, Angular automatically marks the component to ensure it gets updated the next time change detection runs.

Furthermore:

Effects always execute asynchronously, during the change detection process.

Therefor, the easiest way to achieve your goal in Angular 16 is by creating a dummy component, and calling the change detection on it.

@Component({
  selector: 'test-component',
  template: ``,
})
class TestComponent {}

describe('ServiceA', () => {
  let serviceA: ServiceA;
  let serviceB: ServiceB;
  // We add the fixture so we can access it across specs
  let fixture: ComponentFixture<TestComponent>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        ServiceA,
        ServiceB,
      ]
    });

    serviceA = TestBed.inject(ServiceA);
    serviceB = TestBed.inject(ServiceB);
    fixture = TestBed.createComponent(TestComponent);
  });

  it('should update the signalA value when publicMethodA is called but not call the publicMethodB of ServiceB', () => {~
    jest.spyOn(serviceB, 'publicMethodB');
    serviceA.publicMethodA(1);

    expect(serviceA['signalA']()).toEqual(1);
    expect(serviceB.publicMethodB).not.toHaveBeenCalled();
  });

  it('should update the signalA value and call publicMethodB of the ServiceB when publicMethodA', () => {
    jest.spyOn(serviceB, 'publicMethodB');
    serviceA.publicMethodA(1);
    fixture.detectChanges();

    expect(serviceA['signalA']()).toEqual(1);
    expect(serviceB.publicMethodB).toHaveBeenCalled();
  });
});

To improve our knowledge, lets understand what the TestBedd.flushEffects method actually does:

  /**
   * Execute any pending effects.
   *
   * @developerPreview
   */
  flushEffects(): void {
    this.inject(EffectScheduler).flush();
  }

So this just triggers a normal flush event with the EffectScheduler.
Digging a bit more leads to this file:

export abstract class EffectScheduler {
  /**
   * Schedule the given effect to be executed at a later time.
   *
   * It is an error to attempt to execute any effects synchronously during a scheduling operation.
   */
  abstract scheduleEffect(e: SchedulableEffect): void;

  /**
   * Run any scheduled effects.
   */
  abstract flush(): void;

  /** @nocollapse */
  static ɵprov = /** @pureOrBreakMyCode */ ɵɵdefineInjectable({
    token: EffectScheduler,
    providedIn: 'root',
    factory: () => new ZoneAwareEffectScheduler(),
  });
}

/**
 * A wrapper around `ZoneAwareQueueingScheduler` that schedules flushing via the microtask queue
 * when.
 */
export class ZoneAwareEffectScheduler implements EffectScheduler {
  private queuedEffectCount = 0;
  private queues = new Map<Zone | null, Set<SchedulableEffect>>();
  private readonly pendingTasks = inject(PendingTasks);
  private taskId: number | null = null;

  scheduleEffect(handle: SchedulableEffect): void {
    this.enqueue(handle);

    if (this.taskId === null) {
      const taskId = (this.taskId = this.pendingTasks.add());
      queueMicrotask(() => {
        this.flush();
        this.pendingTasks.remove(taskId);
        this.taskId = null;
      });
    }
  }

  private enqueue(handle: SchedulableEffect): void {
    ...
  }

  /**
   * Run all scheduled effects.
   *
   * Execution order of effects within the same zone is guaranteed to be FIFO, but there is no
   * ordering guarantee between effects scheduled in different zones.
   */
  flush(): void {
    while (this.queuedEffectCount > 0) {
      for (const [zone, queue] of this.queues) {
        // `zone` here must be defined.
        if (zone === null) {
          this.flushQueue(queue);
        } else {
          zone.run(() => this.flushQueue(queue));
        }
      }
    }
  }

During an Angular app normal functioning, effects will be scheduled for execution via the ZoneAwareEffectScheduler. The engine will then deal with each effect as they are executed (ChangeDetection, browser events and others trigger the execution).

What the TestBed.flushEffects is it provides us with a way to run these effects but exposing an entry point to execute them on the ZoneAwareEffectScheduler.

4

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật