【问题标题】:Testing an a observable subject from a backend service从后端服务测试一个可观察的主题
【发布时间】:2021-05-30 04:19:04
【问题描述】:

我决定需要在部署代码之前通过实际测试代码来改进我的 Angular 应用程序。我有 C#/.Net 背景,并且知道在这种情况下进行单元测试的方法。

到目前为止,我为我的 Angular 应用程序编写的大多数单元测试都相对简单,但测试一个可观察的主题已成为一个绊脚石。

我正在为其编写测试的代码包括以下内容:

  • 从 Azure 中运行的 .Net 应用程序获取新闻项 (JSON) 的服务
  • 使用上述内容的组件

服务使用 HTTP GET 请求检索新闻项目并更新可观察的主题,组件订阅主题并处理主题提供的 JSON。

我遇到的问题是测试从服务订阅主题并将处理移交给另一个函数的函数。

这是服务的代码:

export class NewsService {

  userObject!: UserSettings;
  localeId = 'da';
  data: any;

  public newsFeed!: News[];
  public newsFeedSubject = new Subject<any>();
  public newsFeedTimer = new Subscription();

  constructor(private http: HttpClient) {

    this.newsFeedTimer = this.timerSubscription();
    console.log('constructor called');
  }

  public timerSubscription(): Subscription {
    return timer(globalConstants.interval10Secs, globalConstants.interval2Hours).subscribe(() => {
      this.updateNewsFeedsSubject();
    });
  }

  public updateNewsFeedsSubject(): void {
    console.log(`Calling newsfeed service`);

    this.getNewsFeed(this.localeId).subscribe(newsFeed => {
      this.newsFeedSubject.next(newsFeed);

      console.log('NewsFeedTimer updated, newsfeed updated');
    });
  }

  public getNewsFeed(locale: string): Observable<News[]> {
    return this.http.get<News[]>(`${environment.baseURL}/${environment.newsServiceUrl}?locale=${this.localeId}`).pipe(
      tap(data => console.log('NewsFeed Items retrieved: ', data)),
      tap(() => console.log('getNewsFeed: HTTP Request executed'))
    );
  }
}

这是组件的代码:

export class HomeComponent implements OnInit {

  // Newsfeed service
  public newsFeed!: News[];

  constructor(private newsFeedService: NewsService) { }

  ngOnInit(): void {

    this.newsFeedServiceSubscribe();
  }

  public newsFeedServiceSubscribe(): void {
    this.newsFeedService.newsFeedSubject.subscribe(newsFeed => {
      this.updateNewsFeed(newsFeed);
      console.log('newsFeedServiceSubscribe function has been called');
    });
  }

  public updateNewsFeed(newsFeed: any): void {
    this.newsFeed = newsFeed;
    console.log('updateNewsFeed function has been called');
    console.log(newsFeed);
  }
}

最后我(可能是拙劣的)尝试测试 newsFeedServiceSubscribe 函数:

  it('#newsFeedServiceSubscribe should call on the subscribe function on the updateNewsFeed subject', fakeAsync(() => {
    const spyNewsFeedSubjectSubj = spyOn(newsService.newsFeedSubject, 'subscribe');    
    spyOn(component, 'updateNewsFeed');
    
    expect(spyNewsFeedSubjectSubj.calls.any()).toBe(false,'subject should not have been called');
    expect(component.updateNewsFeed).toHaveBeenCalledTimes(0);

    component.newsFeedServiceSubscribe();
    fixture.detectChanges();
    
    newsService.newsFeedSubject.next(mockNewsfeedTestData);
    fixture.detectChanges();

    expect(spyNewsFeedSubjectSubj.calls.any()).toBe(true,'subject should have been called');
    expect(component.updateNewsFeed).toHaveBeenCalledTimes(1);
  }));

实际代码运行良好,新闻项目从主题“返回”并处理得很好,但我似乎无法正确测试。当我调用主题的“next”方法时,我希望主题(在服务中)被更新,然后我希望 newsFeedServiceSubscribe 函数中的订阅实际调用组件中的 updateNewsFeed 函数。

不幸的是,这似乎不起作用。如何实际测试该主题的订阅是否返回数据并调用 updateNewsFeed?

谢谢, 比亚恩

【问题讨论】:

    标签: angular unit-testing jasmine karma-jasmine rxjs-observables


    【解决方案1】:

    如果不查看整个规范文件,很难判断您是如何获得NewsService 的实例的。我的猜测是规范中使用的NewsService 可能与注入HomeComponent 的实例不同

    使用beforeEach,您可以从TestBed 获取NewsService 的实例。

    let fixture: ComponentFixture<AppComponent>;
    let newsService: NewsService;
    let component: AppComponent;
    
    const mockNewsfeedTestData: any = [];
      
    beforeEach(() => {
      fixture = TestBed.createComponent(AppComponent);
      component = fixture.componentInstance;
    
      // Get instance of NewsService
      // Newer Angular versions use TestBed.inject
      newsService = TestBed.get(NewsService);
    });
    
    

    那么测试用例会是这样的......

    it("#newsFeedServiceSubscribe should call on the subscribe function on the updateNewsFeed subject", fakeAsync(() => {
      spyOn(component, "updateNewsFeed");
    
      component.newsFeedServiceSubscribe();
      newsService.newsFeedSubject.next(mockNewsfeedTestData);
    
      expect(component.updateNewsFeed).toHaveBeenCalledTimes(1);
    }));
    

    您提供的代码中的工作示例堆栈闪电战...

    https://stackblitz.com/edit/unit-test-subsribe-01?file=src%2Fapp%2Fapp.component.spec.ts

    【讨论】:

      【解决方案2】:

      我会建议一种更简单的方法。在您的 component.spec 文件中,只需创建一个模拟服务并返回模拟数据,然后为此运行测试。像这样:

      // In your component.spec file, 
      
      @Injectable()
      class MockService extends RealService {
        yourOriginalServiceMethod() {
          return of(mockData);
          // Here mockData can be any mocked-data. It should be of whatever the type your 
             original method in the service returns. Like an object 
         // 'of' is the RXJS operator. It will turn your mockData to an Observable so that when you run the test case, it will be subscribed without any issue. 
        }
      }
        
      beforeEach(() => {
        fixture = TestBed.createComponent(AppComponent);
        component = fixture.componentInstance;
      
        realService = TestBed.get(RealService); // this is important to test the subscription error scenario
      });
      
      describe('AppComponent', () => { // 2
        beforeEach(async(() => { // 3
          TestBed.configureTestingModule({
            declarations: [
              AppComponent
            ],
            providers: [
              {
                 provide: MockService,
                 useClass: RealService
              }
            ]
          }).compileComponents();
        }));
      
      // Now your test case, fakeAsync or async - nothing is required.  
      
      it("component #newsFeedServiceSubscribe() method for successful subscription",() => {
        spyOn(component, "newsFeedServiceSubscribe").and.callThrough();
        component.newsFeedServiceSubscribe();
        expect(component.updateNewsFeed).toHaveBeenCalled();
      
        // THis method will clear the successful subscription scenario
      });
      
      
      it("component #newsFeedServiceSubscribe() method for failed subscription",() => {
      
        // This line will call the service and instead of returning mockData, it will fail it.
        spyOn(realService, 'yourMethodName').and.returnValue(throwError({status: 500}));
        
        // Rest is the same
        spyOn(component, "newsFeedServiceSubscribe").and.callThrough();
        component.newsFeedServiceSubscribe();
        expect(component.updateNewsFeed).toHaveBeenCalled();
      
        // THis method will clear the failed subscription scenario
      });
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-12-11
        • 1970-01-01
        相关资源
        最近更新 更多