以前的項目測試採用的是使用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(); });
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); }
/** * 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 }); } }
/** * 全部的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); }
/** * 註冊模擬接口 * @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; }
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 }); } ); }
/** * MOCK服務。 */mockHttpService: MockApiService; /** * 注入MOCK服務 ** @param mockHttpService 模擬HTTP服務 */ injectMockHttpService(mockHttpService: MockApiService): void { this.mockHttpService = mockHttpService; this.registerGetByPaperId(); }
/** * 註冊模擬接口 * @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
/** * 返回供測試用的觀察者 * 若是當前爲測試過程當中,則調用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; } } }
/** * 模擬後臺接口模塊 * 因爲MockHttpClient依賴於MockApiService * 因此必須先聲明MockApiService,而後再聲明MockHttpClient * 不然將產生依賴異常 * 每增長一個後臺模擬接口,則須要對應添加到providers。 * 不然模擬接口將被angular的搖樹優化搖掉,從而使得其註冊方法失敗 */ @NgModule({ providers: [ MockApiService, {provide: HttpClient, useClass: MockHttpClient}, AnswerSheetApi ] }) export class MockApiModule { }