【问题标题】:Angular 2 Testing a component with an observable without mockingAngular 2 使用 observable 测试组件而不进行模拟
【发布时间】:2017-09-14 16:55:29
【问题描述】:

我一直在尝试使用 Jasmine 为 Angular 2 项目创建集成测试。我可以很好地模拟服务和数据并运行适当的单元测试,但现在我想测试以确保我的组件可以使用该服务来实际连接到后端 API。到目前为止,我可以确认我的组件和服务中的方法都是由我的间谍触发的,但是我的组件中的 .subscribe(....) 方法在测试完成/失败之前不会完成。我的规范代码如下:

import { async, ComponentFixture, TestBed, inject, fakeAsync } from '@angular/core/testing';
import { DebugElement } from "@angular/core";
import { By } from "@angular/platform-browser";

import { ReviewComponent } from './review.component';
import { FormsModule } from '@angular/forms';
import { UrlService } from 'app/shared/services/url.service';
import { HttpModule, Http } from '@angular/http';
import { ReviewService } from "./review.service";

describe('CommunicationLogComponent', () => {
  let component: ReviewComponent;
  let fixture: ComponentFixture<ReviewComponent>;
  let serviceSpy: jasmine.Spy;
  let componentSpy: jasmine.Spy;
  let service: ReviewService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [FormsModule, HttpModule],
      providers: [UrlService],
      declarations: [ReviewComponent]
    })
      .compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(ReviewComponent);
    component = fixture.componentInstance;
    component.processId = 6;
    service = fixture.debugElement.injector.get(ReviewService);
    componentSpy = spyOn(component, "ngOnInit").and.callThrough();
    serviceSpy = spyOn(service, 'getReviewByProcess').and.callThrough();
  });

  it('should create component', () => {
    expect(component).toBeTruthy();
  });
  it('should not have called ngOnInit() yet', () => {
    expect(componentSpy).not.toHaveBeenCalled();
  });

  it('should make a call to ngOnInit() and by extension getReviewByProcess', () => {
    fixture.detectChanges();
    expect(componentSpy).toHaveBeenCalled();
    expect(serviceSpy).toHaveBeenCalled();
  });
  //ISSUES HERE
  it('should show review after the getReviewByProcess resolves', async(() => {
    fixture.detectChanges();
    fixture.whenStable().then(() => {
      expect(componentSpy).toHaveBeenCalled();
      fixture.detectChanges();
      expect(component.Reviews[0]).not.toBe(undefined);
    });
  }), 5000);        
});

在标有“//ISSUES HERE”之前的所有测试都运行良好并通过。

我正在使用 async() 并等待夹具变得稳定,然后再尝试完成我的测试。它还通过 componentSpy 确认已调用 ngOnInit(它将连接到后端并订阅 Review)。

我的组件代码如下所示:

import { Component, EventEmitter, Input, OnInit } from '@angular/core';

//Service 
import { ReviewService } from "app/process/monitoring/shared/components/review/review.service";

//Classes
import { Review } from "app/process/monitoring/shared/components/review/review";

@Component({
  providers: [
    ReviewService
  ],
  selector: 'monitoring-review',
  templateUrl: './review.component.html',
  styleUrls: ['./review.component.css']
})
export class ReviewComponent implements OnInit {

  public Review: Review = new Review();
  public Reviews: Review[] = [];

  @Input() public processId: number;

  constructor(
    private ReviewService: ReviewService
  ) { }


  ngOnInit(): void {
    this.ReviewService.getReviewByProcess(this.processId, true).first().subscribe(
      Reviews => {
        if (Reviews) {
          this.Reviews = Reviews //doesn't trigger until test is finished
        }
      },
      error => console.log(error)
    );
  }
}

最后我的 ReviewService 看起来像这样:

import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Headers, RequestOptions, RequestMethod, URLSearchParams } from '@angular/http';
import { Observable } from 'rxjs/Observable';

import 'rxjs/add/observable/throw';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';

// App-wide Services
import { UrlService } from 'app/shared/services/url.service';

// Classes
import { Review } from "app/process/monitoring/shared/components/review/review";


@Injectable()
export class ReviewService {

  private devUrl: string;
  private jsonHeaders: Headers = new Headers({ 'Content-Type': 'application/json' });
  private prodUrl: string;
  private reviewPath: string = 'monitoring/program/';

  constructor(
    private urlService: UrlService,
    private http: Http
  ) {
    this.devUrl = this.urlService.getUrl('dev').url;
    this.prodUrl = this.urlService.getUrl('prod').url;
  }

  getReviewByProcess(processId: number, isProd: boolean = false): Observable<Review[]> {
    let targetUrl: string = (isProd ? this.prodUrl : this.devUrl) + this.reviewPath + "/" + processId;

    return this.http.get(targetUrl, { withCredentials: true })
      .map((res: Response) => res.json())
      .catch((error: any) => Observable.throw(error.json().error || 'Server error')
      );
  };
}

使用 Karma 调试器,我可以确认我的测试调用了 ngOnInit,然后调用服务,但它永远不会触及我的组件的内部订阅(标有“//不触发”),直到在我的测试完成后。

总结一下:我想创建一个测试,该测试可以使用“实时”后端收集我的数据来测试我的组件与我的服务的集成。那就是服务,我的组件不会嘲笑任何东西。我可以确认正在使用我的方法,但是在我的 ReviewComponent 的 ngOnInit() 方法中找到的 .subscribe(...) 直到测试完成/失败后才完成。

【问题讨论】:

    标签: angular typescript jasmine integration-testing observable


    【解决方案1】:

    您需要返回 observable 并在测试中订阅它,以便异步方法等待它。例如,以下假设测试会起作用。

    it('', async(() => {
      observable().subscribe(
        (review) => {
           expect(component.Reviews[0]).not.toBe(undefined)
        }
      )
    }));
    

    问题是您没有等待异步调用在测试中执行。您也可以稍微改变 angular http 服务,即:

    import { Http, HttpModule, BaseRequestOptions, XHRBackend, HttpModule, ConnectionBackend } from '@angular/http';
    const httpSubject = new Subject()
    const http$ = httpSubject.asObservable()
    const httpFactory = (backend, options) => {
      const newHttp = new Http(backend, options);
      const helperHttp = new Http(backend, options);
      newHttp.get = function(strng, options) {
        return helperHttp.get(strng, options).do((response) => {
          setTimeout(() => {
            httpSubject.next({});
          }, 300);
        });
      };
      return newHttp;
    }
    
    TestBed.configureTestingModule({
      imports: [HttpModule, FormModule],
      providers: [
        UrlService,
        ConnectionBackend,
        BaseRequestOptions
        {
          provide: Http,
          useFactory: httpFactory,
          deps: [ XHRBackend, BaseRequestOptions] 
        }
      ],
      declarations: [ReviewComponent]
    })
    
    it('should show review after the getReviewByProcess resolves', 
      async(() => {
        http$.subscribe(() => {
          expect(componentSpy).toHaveBeenCalled();
          expect(component.Reviews[0]).not.toBe(undefined);
        })
        fixture.detectChanges();
    })); 
    

    【讨论】:

    • 即使在我放置了适当的 url、选项等之后,您作为示例设置的代码也会出现调用堆栈错误。
    • @LandSharks 对不起,我已经更新了它,它在本地为我工作。试一试,如果您仍有问题,我可以将我创建的测试版本发送给您。
    • @LandSharks 如果您设法尝试或找到其他解决方案,请发表评论。很高兴知道。
    【解决方案2】:

    您需要您的服务返回模拟数据。类似的东西:

    spyOn(service, 'getReviewByProcess')
       .and.returnValue(Observable.of(new Array<Review>()));
    

    【讨论】:

    • 我不想从我的 Web API 返回模拟数据,而是返回合法数据。这只是茉莉花无法支持的事情,我应该使用量角器寻找解决方案吗?
    • 这是单元测试,它应该被限定并专用于特定的组件/功能,你需要模拟环境来存档它。如果你想模拟 http,你可以使用 MockBackend 模拟 http 服务级别。如果您需要调用 Web API 进行测试 - 使用量角器进行端到端测试,但具有 angular 2 单元测试的强大功能,我认为不需要 e2e。
    • 那么 Jasmine 不支持集成测试吗?我在 angular 2 的主文档视线中注意到:angular.io/docs/ts/latest/guide/… 他们正在做我想做的事情,但缺少 http 连接。他们的对象中的数据是静态的。
    • 你可以做任何事情,例如您还可以测试 HTML 输入元素是否具有“必需”验证器属性:D
    猜你喜欢
    • 2017-07-01
    • 2017-02-16
    • 2016-11-23
    • 1970-01-01
    • 2019-05-30
    • 2016-04-09
    • 2017-08-17
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多