首页 > 解决方案 > 如何正确地将 HttpClient 注入依赖项?

问题描述

我开始学习测试 Angular 服务。Angular测试指南中有一个官方示例。但在示例中,一个服务依赖于另一个服务的更简单版本。

  /// HeroService method tests begin ///
  describe('#getHeroes', () => {
    let expectedHeroes: Hero[];

    beforeEach(() => {
      heroService = TestBed.inject(HeroService);
      expectedHeroes = [
        { id: 1, name: 'A' },
        { id: 2, name: 'B' },
       ] as Hero[];
    });

    it('should return expected heroes (called once)', () => {
      heroService.getHeroes().subscribe(
        heroes => expect(heroes).toEqual(expectedHeroes, 'should return expected heroes'),
        fail
      );
      const req = httpTestingController.expectOne(heroService.heroesUrl);
      expect(req.request.method).toEqual('GET');
      req.flush(expectedHeroes);
    });

我有一个 TerritoryApiService ,其中包含一组用于处理区域列表的方法。

@Injectable({
  providedIn: 'root'
})
export class TerritoryApiService {
  private nameApi: string = 'territory';
  constructor(private apiclient : ApiclientService) {}

public GetStaffTerritories(dateFrom: Date, dateTo: Date) {
    let parameters = new Map();
    parameters.set('dateFrom', dateFrom.toISOString());
    parameters.set('dateTo', dateTo.toISOString());
    return this.apiclient.doPostT<StaffTerritory[]>(this.nameApi, 'GetStaffTerritories', parameters);
  }
}

TerritoryApiService 依赖于 ApiClientService,将包含 URL 的 HttpClient 和 AppConfig 传递给 ApiClientService。

@Injectable({
  providedIn: 'root'
})
export class ApiclientService {
  private apiurl: string;
  constructor(private http: HttpClient, @Inject(APP_CONFIG) config: AppConfig) { 
    this.apiurl = config.apiEndpoint;
  }

public doPostT<T> (url: string, method :string, parameters: Map<string,string> ) {
    let headers = new HttpHeaders();
    let httpParams = new HttpParams();
    if (parameters != undefined) {
      for (let [key, value] of parameters) {
        httpParams = httpParams.append(key, value);
    }
    } 
    return this.http.post<T>(this.apiurl +'/v1/' + url + '/' + method, httpParams, {
      headers: headers,
      params: httpParams
    })
  }
}

const appConfig: AppConfig = {
    apiEndpoint: environment.apiEndpoint
  };

export const environment = {
  production: false, 
  apiEndpoint: 'https://localhost:44390/api'
};

请告诉我如何正确准备测试(配置所有依赖项)?因为现在我有两种情况:

1.如果我只是在provide部分指定ApiclientService,那么测试通过并报错,因为appConfig未定义(URL变为'undefined/v1/territory/GetStaffTerritories')

TestBed.configureTestingModule({
      imports: [ HttpClientTestingModule ],
      providers: [ 
        TerritoryApiService, 
        ApiclientService,
        { provide: APP_CONFIG, useValue: APP_CONFIG } 
      ]
    });
  1. 如果我指定使用什么作为 ApiclientService,那么我需要显式创建 HttpClient 并将其传递给构造函数。在这种情况下,测试中会出现 post 方法未定义的错误。那么需要创建HttpClient吗?

    常量 appConfig: AppConfig = { apiEndpoint: environment.apiEndpoint }; TestBed.configureTestingModule({ imports: [ HttpClientTestingModule ], providers: [ TerritoryApiService, { provide: ApiclientService, useValue: new ApiclientService(httpClient, appConfig) }, { provide: APP_CONFIG, useValue: APP_CONFIG } ]});

完整的测试代码

describe('TerritoryApiService', () => {
  let service: TerritoryApiService;
  let httpClient: HttpClient;
  let httpTestingController: HttpTestingController;

  const staffTerritoriesStub: StaffTerritory[] = [{ 
    id: 1, 
    name: 'StaffTerritory', 
    regionCode: 29, 
    createDate: new Date(),
    creatorId: 0,
    dateFrom: new Date(),
    dateTo: null,
    dateToSelect: new Date(),
    dateFromChanger: 0,
    dateToChanger: null, 
  }];

  const appConfig: AppConfig = {
    apiEndpoint: environment.apiEndpoint
  };

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [ HttpClientTestingModule ],
      providers: [ 
        TerritoryApiService, 
        { provide: ApiclientService, useValue: new ApiclientService(httpClient, appConfig) },
        { provide: APP_CONFIG, useValue: APP_CONFIG } 
      ]
    });
    httpClient = TestBed.inject(HttpClient);
    httpTestingController = TestBed.inject(HttpTestingController);
    service = TestBed.inject(TerritoryApiService);
  });

  afterEach(() => {
    httpTestingController.verify();
  });


  describe('#GetStaffTerritories', () => {
    beforeEach(() => {
      service = TestBed.inject(TerritoryApiService);
    });

    it('should return expected heroes (called once)', () => {
      service.GetStaffTerritories(new Date(), new Date()).subscribe(
        staffTerritories => expect(staffTerritories).toEqual(staffTerritoriesStub, 'should return expected staffTerritories'),
        fail
      );
      const req = httpTestingController.expectOne(appConfig.apiEndpoint + '/v1/' + 'territory' + '/' + 'GetStaffTerritories');
      req.flush(staffTerritoriesStub);
    });
  });
});

标签: angulartypescriptkarma-jasmine

解决方案


我认为您提供的内容是AppConfig错误的,在您的情况下,我不会嘲笑ApiclientServicewith ,new ApiclientService(httpClient, appConfig)因为您提供的是HttpClientwith的实际实现HttpClientTestingModule

试试这个:

describe('TerritoryApiService', () => {
  let service: TerritoryApiService;
  let httpClient: HttpClient;
  let httpTestingController: HttpTestingController;

  const staffTerritoriesStub: StaffTerritory[] = [{ 
    id: 1, 
    name: 'StaffTerritory', 
    regionCode: 29, 
    createDate: new Date(),
    creatorId: 0,
    dateFrom: new Date(),
    dateTo: null,
    dateToSelect: new Date(),
    dateFromChanger: 0,
    dateToChanger: null, 
  }];

  const appConfig: AppConfig = {
    apiEndpoint: environment.apiEndpoint // make sure environment.apiEndpoint is defined
  };

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [ HttpClientTestingModule ],
      providers: [ 
        TerritoryApiService, 
        ApiclientService, // provide the actual ApiclientService since you have the dependencies 
        { provide: AppConfig, useValue: appConfig } // this line needs to change to this 
      ]
    });
    httpClient = TestBed.inject(HttpClient);
    httpTestingController = TestBed.inject(HttpTestingController);
    service = TestBed.inject(TerritoryApiService);
  });

  afterEach(() => {
    httpTestingController.verify();
  });


  describe('#GetStaffTerritories', () => {
    beforeEach(() => {
      service = TestBed.inject(TerritoryApiService);
    });

    it('should return expected heroes (called once)', (done) => { // add done here to have a handle to let Jasmine know when we are done with our assertions
      service.GetStaffTerritories(new Date(), new Date()).subscribe(
        staffTerritories => {
          expect(staffTerritories).toEqual(staffTerritoriesStub, 'should return expected 
          staffTerritories');
          done(); // I would put a done here to ensure that the subscribe block was traversed
       },
        fail
      );
      const req = httpTestingController.expectOne(appConfig.apiEndpoint + '/v1/' + 'territory' + '/' + 'GetStaffTerritories');
      req.flush(staffTerritoriesStub);
    });
  });
});

推荐阅读