【问题标题】:Angular 11 Testing with Jest - Aync error with rxjs timerAngular 11 测试与 Jest - rxjs 计时器的异步错误
【发布时间】:2021-05-02 08:54:28
【问题描述】:

我在编写测试时遇到以下错误: “超时 - 在 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);
  }
}

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

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

谢谢!

【问题讨论】:

  • 谢谢。我一直试图让它运行,但不知何故只添加它('应该停止计数器并发出事件',函数(完成){最后调用完成()并不能解决问题。
  • , async (done) => { ... }, 100000);改变默认的5000怎么样
  • 不幸的是,这也不起作用:它('应该停止计数器并发出事件',async(完成)=> { 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() ; 期望(component.stopped.emit).toHaveBeenCalled(); done(); }, 100000);
  • 我会说进行此类测试的更好方法是提供组件正在使用的服务的模拟实例。因此,您可以提供服务的实现,该服务将在同一时刻返回并完成 observable,而不是等待任何事情。组件的单元测试不应依赖于服务的特定实现

标签: javascript angular typescript ts-jest


【解决方案1】:

查看您的 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();
  }));

【讨论】:

  • 谢谢。您的评论和支持确实帮助我更好地理解了测试 rxjs 的工作原理。实际上我的解决方案也有效,但我在提到的测试之前运行了一个测试。在此测试中,订阅从未取消订阅。因此我添加了让一切运行良好:ngOnDestroy() { if (this.counter) { this.counter.unsubscribe(); } }
  • 嗯,好的。乐意效劳。即使这不是真正的问题。是的,取消订阅非常重要。我曾经有一个抽象组件,它包含一个订阅数组并处理每个订阅的取消订阅,并且只是扩展了每个处理 observables 的组件,所以我没有忘记。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-03-14
  • 2020-03-15
  • 2020-11-24
  • 1970-01-01
  • 2020-12-19
  • 2018-11-21
相关资源
最近更新 更多