使用MockHttpClientModule替換HttpClientModule學習心得

前言

以前的項目測試採用的是使用Stub替換原有的組件進行測試,現在的問卷系統採用了新的思想,就是使用使用MockHttpClientModule替換HttpClientModule,先後臺接口徹底統一,更接近於真實請求,本文以總結學習心得爲主,總結一下該方法的思路。正則表達式

方法

首先看一下要測試的方法:json

/**
 * 經過試卷ID獲取答卷 
 * 1. 若是存在還沒有完成的答卷,則返回還沒有完成的答卷 
 * 2. 若是不存在還沒有完成的答卷,則新建一個答卷返回 
 * @param id 試卷ID
 */getByPaperId(id: number): Observable<AnswerSheet> {
  return this.httpClient.get<AnswerSheet>(`${this.baseUrl}/${id}`);
}

測試方法:api

beforeEach(() => {
  TestBed.configureTestingModule({
    imports: [
      MockApiModule
    ],
 providers: [
      MockHttpClient
    ]
  });
 service = TestBed.inject(AnswerSheetService);
});
it('測試模擬接口服務是否生效', () => {
  expect(service).toBeTruthy();
 let called = false;
 service.getByPaperId(123).subscribe(data => {
    expect(data.id).toEqual(123);
 called = true;
 });
 getTestScheduler().flush();
 expect(called).toBeTrue();
});

MockHttpClient

export class MockHttpClient {
  constructor(private mockApiService: MockApiService) {
  }
  get<T>(url: string, options?: {
    headers?: HttpHeaders | {
      [header: string]: string | string[];
 };
 params?: HttpParams | {
      [param: string]: string | string[];
 };
 }): Observable<T> {
    return this.mockApiService.get<T>(url, options);
 }

MockApiService

get方法

/**
 * get方法 * @param url 請求地址
 * @param options 選項
 */ 
 get<T>(url: string, options = {} as {
    headers?: HttpHeaders | {
      [header: string]: string | string[];
 };
 params?: HttpParams | {
      [param: string]: string | string[];
 };
 }): Observable<T> {
    return this.request<T>('get', url, {
      observe: 'response',
 responseType: 'json',
 headers: options.headers,
 params: options.params
 });
 }
}

request

/**
 * 全部的GETPOSTDELETEPUTPATCH方法最終均調用request方法。 * 若是當前request不可以知足需求,則請移步angular官方提供的HttpClient * * 該方法先根據method進行匹配,接着根據URL進行正則表達式的匹配。 * 匹配成功後將參數傳入接口並獲取模擬接口的返回值 * * @param method 請求方法
 * @param url 請求地址
 * @param options 選項
 */
 request<R>(method: string, url: string, options: {
  body?: any;
 headers?: HttpHeaders | {
    [header: string]: string | string[];
 };
 reportProgress?: boolean;
 observe: 'response';
 params?: HttpParams | {
    [param: string]: string | string[];
 };
 responseType?: 'json';
 withCredentials?: boolean;
}): Observable<R> {
  let result = null as R;
 let foundCount = 0;
 const urlRecord = this.routers[method] as Record<string, RequestCallback<any>>;
 for (const key in urlRecord) {
    if (urlRecord.hasOwnProperty(key)) {
      const reg = new RegExp(key);
 if (reg.test(url)) {
        const callback = urlRecord[key] as RequestCallback<R>;
 callback(url.match(reg), options.params, options.headers, (body) => {
          result = body;
 foundCount++;
 if (foundCount > 1) {
            throw Error('匹配到了多個URL信息,請檢定注入服務的URL信息,URL信息中存在匹配衝突');
 }
        });
 }
    }
  }
  if (null === result) {
    throw Error('未找到對應的模擬返回數據,請檢查url、method是否正確,模擬注入服務是否生效');
 }
  return testingObservable(result);
}

registerMockApi

/**
 * 註冊模擬接口 * @param url 請求地址
 * @param method 請求方法
 * @param callback 回調
 */
registerMockApi<T>(method: RequestMethodType, url: string, callback: RequestCallback<T>): void {
  if (undefined === this.routers[method] || null === this.routers[method]) {
    this.routers[method] = {} as Record<string, RequestCallback<T>>;
 }
  if (isNotNullOrUndefined(this.routers[method][url])) {
    throw Error(`在地址${url}已存在${method}的路由記錄`);
 }
  this.routers[method][url] = callback;
}

AnswerSheetApi

registerGetByPaperId()

private baseUrl = '/answerSheet';

/**
 * 註冊GetByPaperId接口 
 * 註冊完成後,當其它的服務嘗試httpClient時 
 * 則會按此時註冊的方法、URL地址進行匹配 
 * 匹配成功後則會調用在此聲明的回調函數,同時將請求地址、請求參數、請求header信息傳過來 
 * 咱們最後根據接收的參數返回特定的模擬數據,該數據與後臺的真實接口保持嚴格統一 */
 registerGetByPaperId(): void {
  this.mockHttpService.registerMockApi<AnswerSheet>(
    `get`,
 `^${this.baseUrl}/(d+)$`,
 (urlMatches, httpParams, httpHeaders, callback) => {
      const id = urlMatches[1];
 callback({
        id: +id
      });
 }
  );
}

injectMockHttpService

/**
 * MOCK服務。 
 */mockHttpService: MockApiService;
 
 
/**
 * 注入MOCK服務 
 ** @param mockHttpService 模擬HTTP服務
 */
injectMockHttpService(mockHttpService: MockApiService): void {
  this.mockHttpService = mockHttpService;
 this.registerGetByPaperId();
}

MockApiService

constructor()

/**
 * 註冊模擬接口 
 * @param clazz 接口類型
 */
 static registerMockApi(clazz: Type<Api>): void {
  this.mockApiRegisters.push(clazz);
}

/**
 * 循環調用從而完成全部的接口註冊 */
 constructor() {
 MockApiService.mockApiRegisters.forEach(api => {
 const instance = new api();
 instance.injectMockHttpService(this);
 });
}

// AnswerSheetApi
MockApiService.registerMockApi(AnswerSheetApi);ide

testingObservable

/**
 * 返回供測試用的觀察者 
 * 若是當前爲測試過程當中,則調用cold方法返回觀察者將不出拋出異常。 
 * 不然使用of方法返回觀察者 
 * @param data 返回的數據
 * @param delayCount 延遲返回的時間間隔
 */
 export function testingObservable<T>(data: T, delayCount = 1): Observable<T> {
  try {
    let interval = '';
 for (let i = 0; i < delayCount; i++) {
      interval += '---';
 }
    return cold(interval + '(x|)', {x: data});
 } catch (e) {
    if (e.message === 'No test scheduler initialized') {
      return of(data).pipe(delay(delayCount * 500));
 } else {
      throw e;
 }
  }
}

MockApiModule

/**
 * 模擬後臺接口模塊 
 * 因爲MockHttpClient依賴於MockApiService 
 * 因此必須先聲明MockApiService,而後再聲明MockHttpClient 
 * 不然將產生依賴異常
 * 每增長一個後臺模擬接口,則須要對應添加到providers。 
 * 不然模擬接口將被angular的搖樹優化搖掉,從而使得其註冊方法失敗 
 */
 @NgModule({
  providers: [
    MockApiService,
 {provide: HttpClient, useClass: MockHttpClient},
 AnswerSheetApi
  ]
})
export class MockApiModule {
}

總結

image.png

相關文章
相關標籤/搜索