Angular 中的单元测试:使用 Jasmine 模拟可观察到的 RxJS

Unit testing in Angular: Mocking RxJS observable with Jasmine

我正在对 Angular 12 组件进行单元测试。该组件在初始化时获取从服务返回的可观察值(参见下面的 thing.service.ts)。它被分配给一个主题,该主题通过 async 管道显示在 html 模板中(参见下面的 app.component.html)。

AppComponent.ts

export class AppComponent  {
  public errorObjectSubject = null;
  public thingsSubject: Subject<Array<IThing>> = new Subject();

  constructor(private readonly _service: ThingService) {
    this._getAllProducts();
  }

  private async _getAllProducts(): Promise<void> {
    this._service.getAllObservableAsync()
      .pipe(take(1),
        catchError(err => {
          this.errorObjectSubject = err;
          return throwError(err);
        })
      ).subscribe(result => { this.thingsSubject.next(result) });
  }
}

模板使用 async 管道订阅 public thingsSubject: Subject<Array<IThing>> = new Subject();

app.component.html

<div>
  <app-thing *ngFor="let thing of thingsSubject | async" [thing]="thing"></app-thing>
</div>

thing.service.ts

  constructor(private readonly _http: HttpClient) { }

  public getAllObservableAsync(): Observable<Array<IThing>> {
    return this._http.get<Array<IThing>>('https://jsonplaceholder.typicode.com/todos'); }

这是测试设置。

app.component.spec.ts

describe('AppComponent', () => {
  let component: AppComponent,
    fixture: ComponentFixture<AppComponent>,
    dependencies: { thingService: ThingServiceStub };

  function getThings(): Array<DebugElement> {
    return fixture.debugElement.queryAll(By.directive(ThingComponentStub));
  }

  beforeEach(async () => {
    dependencies = {
      thingService: new ThingServiceStub()
    };
    await TestBed.configureTestingModule({
      declarations: [AppComponent, ThingComponentStub],
      providers: [
        { provide: ThingService, useValue: dependencies.thingService }
      ]
    }).compileComponents();
  });

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

  describe('on initialisation', () => {
    let getThingsSubject: Subject<Array<IThing>>;

    beforeEach(() => {
      getThingsSubject = new Subject();
      (dependencies.thingService
        .getAllObservableAsync as jasmine.Spy).and.returnValue(
        getThingsSubject.asObservable()
      );
      fixture.detectChanges();
    });

    it('should fetch all of the things', () => {
      //fixture.detectChanges();
      expect(
        dependencies.thingService.getAllObservableAsync
      ).toHaveBeenCalledWith();
    });

    describe('when the things have been fetched', () => {
      beforeEach(fakeAsync(() => {
        getThingsSubject.next()
        // getThingsSubject.next([
        //   {
        //     userId: 1,
        //     id: 1,
        //     title: 'string',
        //     completed: 'string'
        //   }
        // ]);
        //getThingsSubject.pipe().subscribe()

        tick();

        fixture.detectChanges();
      }));

      it('should display the things', () => {
        expect(getThings()[0].componentInstance.product).toEqual({
          name: 'product',
          number: '1'
        });
      });
    });
  });
});

thing.service.stub.ts

export class ProductServiceStub {
    public getAllObservableAsync: jasmine.Spy = jasmine.createSpy('getAllObservableAsync');
  }

我正在尝试测试模板填充内容后它的工作原理 (IThing[])。我有一个 passing 规范调用模拟 observable:

it('should fetch all of the things', () => {
  expect(
    dependencies.thingService.getAllObservableAsync
  ).toHaveBeenCalledWith();
});

然而,当我尝试测试模板时,我遇到了“错误:未捕获(承诺):TypeError:无法读取未定义的属性 'pipe'”:describe('when the things have been fetched'。 =24=]

我不太确定问题出在哪里。这是我如何设置对主题的订阅吗?还是变化检测?

我认为您调用事物的顺序可能是个问题。

试试这个:

describe('AppComponent', () => {
  let component: AppComponent,
    fixture: ComponentFixture<AppComponent>,
    dependencies: { thingService: ThingServiceStub };
  

  function getThings(): Array<DebugElement> {
    return fixture.debugElement.queryAll(By.directive(ThingComponentStub));
  }

  beforeEach(async () => {
    dependencies = {
      thingService: new ThingServiceStub()
    };
    await TestBed.configureTestingModule({
      declarations: [AppComponent, ThingComponentStub],
      providers: [
        { provide: ThingService, useValue: dependencies.thingService }
      ]
    }).compileComponents();
  });

  beforeEach(() => {
    // !! This (.createComponent) is when the constructor is called, so mock the observable
    // before here and change the subject to a BehaviorSubject.
    // Maybe subject will work as well.
    let getThingsSubject = new BehaviorSubject([{ name: 'product', number: '1' }]);
    (dependencies.thingService.getAllObservableAsync as jasmine.Spy).and.returnValue(
       getThingsSubject.asObservable()
    );
    fixture = TestBed.createComponent(AppComponent);
    component = fixture.componentInstance;
  });

  describe('on initialisation', () => {
    let getThingsSubject: Subject<Array<IThing>>;

    it('should fetch all of the things', () => {
      expect(
        dependencies.thingService.getAllObservableAsync
      ).toHaveBeenCalledWith();
    });

    describe('when the things have been fetched', () => {
      // maybe tick and fakeAsync are not needed but `fixture.detectChanges` is
      beforeEach(fakeAsync(() => {

        tick();

        fixture.detectChanges();
      }));

      it('should display the things', () => {
        // !! Add this log for debugging
        console.log(fixture.nativeElement);
        expect(getThings()[0].componentInstance.product).toEqual({
          name: 'product',
          number: '1'
        });
      });
    });
  });
});