1

前言

之前的项目测试采用的是使用Stub替换原有的组件进行测试,如今的问卷系统采用了新的思想,就是使用使用MockHttpClientModule替换HttpClientModule,前后台接口完全统一,更接近于真实请求,本文以总结学习心得为主,总结一下该方法的思路。

方法

首先看一下要测试的方法:

/**
 * 通过试卷ID获取答卷 
 * 1. 如果存在尚未完成的答卷,则返回尚未完成的答卷 
 * 2. 如果不存在尚未完成的答卷,则新建一个答卷返回 
 * @param id 试卷ID
 */getByPaperId(id: number): Observable<AnswerSheet> {
  return this.httpClient.get<AnswerSheet>(`${this.baseUrl}/${id}`);
}

测试方法:

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);

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


锦城
854 声望21 粉丝

好好生活