Angular 11 使用 Jest 进行测试 - rxjs 计时器出现异步错误

Angular 11 Testing with Jest - Aync error with rxjs timer

我在编写测试时遇到以下错误: “超时 - 异步回调未在 jest.setTimeout.Error 指定的 5000 毫秒超时内调用:超时 - 异步回调未在 jest.setTimeout 指定的 5000 毫秒超时内调用。”

这是测试:

beforeEach(
    waitForAsync(() => {
      TestBed.configureTestingModule({
        declarations: [DashboardTimerComponent, FormatTimePipe],
        imports: [BrowserAnimationsModule, ReactiveFormsModule],
        providers: [FormBuilder],
      }).compileComponents();
    }),
  );

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

  it('should stop counter and emit event', fakeAsync(() => {
    spyOn(component.stopped, 'emit');

    component.stopRequested = true;
    component.runningTimer = { timer: 19 };
    fixture.detectChanges();
    

    const button = fixture.debugElement.nativeElement.querySelector('#stop');
    button.click();

    expect(component.timer).toBeNull();
    expect(component.stopped.emit).toHaveBeenCalled();
  }));

这是组件:

@Component({
  selector: 'dashboard-timer',
  templateUrl: './dashboard-timer.component.html',
  providers: [DashboardTimerService],
  animations: [fadeInAnimation],
})
export class DashboardTimerComponent {
  @Input() projects: any;
  @Input() runningTimer: any = null;

  @Output() started = new EventEmitter();
  @Output() stopped = new EventEmitter();

  public form: FormGroup;

  public timer: number = null;

  public stopRequested: boolean = false;

  public counter: Subscription;

  private project: FormControl = new FormControl('');

  private note: FormControl = new FormControl('');

  constructor(
    private dashboardTimerService: DashboardTimerService,
    private fb: FormBuilder,
  ) {}

  ngOnInit() {
    // initialize form
    this.form = this.fb.group({
      project: this.project,
      note: this.note,
    });

    if (this.runningTimer) {
      this.timer = this.runningTimer.timer;
      this.form.controls['project'].setValue(this.runningTimer.project || '');
      this.form.controls['note'].setValue(this.runningTimer.note || '');

      this.counter = this.dashboardTimerService
        .getCounter()
        .subscribe(() => this.timer++);
    }
  }

  /**
   * check if stop requested, stop counter, emit stop to parent component
   */
  stop(): void {
    if (this.stopRequested === false) {
      this.stopRequested = true;

      setTimeout(() => {
        this.stopRequested = false;
      }, 5000);
      return;
    }
    this.stopRequested = false;

    this.counter.unsubscribe();
    this.stopped.emit();
    this.timer = null;
  }
}

错误似乎是由这项服务引起的:

import { Injectable } from '@angular/core';
import { timer } from 'rxjs';

@Injectable()
export class DashboardTimerService {
  getCounter() {
    return timer(0, 1000);
  }
}

我想计时器仍然是 运行,即使我在组件中取消订阅它也是如此。

非常感谢任何解决此问题的想法!

谢谢!

查看您的 stackblitz,该组件实际上提供了服务,这意味着该组件创建了自己的服务实例,并且不使用您在 TestBed 提供程序中提供的模拟值。

所以我的第一个问题是,这个服务真的需要在组件本身上提供吗?

如果是,我看到两个选项:

由于您的数据服务使用 1000 的计时器,因此您实际上需要等待正好那个时间。为此,我会结合使用 fakeAsynctick

it("should stop counter and emit event", fakeAsync(() => {
    spyOn(component.stopped, "emit");

    fixture.detectChanges();

    const button = fixture.debugElement.nativeElement.querySelector("#stop");
    button.click();

    tick(1000); // -> wait till you know that the promise will be resolved
    fixture.detectChanges();

    expect(component.timer).toBeNull();
    expect(component.stopped.emit).toHaveBeenCalled();
  }));

另一种选择是在创建组件后实际覆盖注入的服务,并使用它来覆盖服务实例。

it("should stop counter and emit event", fakeAsync(() => {
    spyOn(component.stopped, "emit");

    // override the actual injected service with your mock values here
    (<any>TestBed.inject(DashboardTimerService)).getCounter = jest.fn().mockReturnValue(of(0))

    fixture.detectChanges();

    const button = fixture.debugElement.nativeElement.querySelector("#stop");
    button.click();

    tick(); // i would still use fakeAsync and tick, since you are handling 
            // observables, which are still async even if they emit directly
    fixture.detectChanges();

    expect(component.timer).toBeNull();
    expect(component.stopped.emit).toHaveBeenCalled();
  }));