前言
之前的项目测试采用的是使用Stub替换原有的组件进行测试,如今的问卷系统采用了新的思想,就是使用使用MockHttpClientModule替换HttpClientModule,前后台接口完全统一,更接近于真实请求,本文以总结学习心得为主,总结一下该方法的思路。
方法
首先看一下要测试的方法:
/**
* 通过试卷ID获取答卷
* 1. 如果存在尚未完成的答卷,则返回尚未完成的答卷
* 2. 如果不存在尚未完成的答卷,则新建一个答卷返回
* @param id 试卷ID
*/getByPaperId(id: number): Observable {
return this.httpClient.get(`${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(url: string, options?: {
headers?: HttpHeaders | {
[header: string]: string | string[];
};
params?: HttpParams | {
[param: string]: string | string[];
};
}): Observable {
return this.mockApiService.get(url, options);
}
MockApiService
get方法
/**
* get方法 * @param url 请求地址
* @param options 选项
*/
get(url: string, options = {} as {
headers?: HttpHeaders | {
[header: string]: string | string[];
};
params?: HttpParams | {
[param: string]: string | string[];
};
}): Observable {
return this.request('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(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 {
let result = null as R;
let foundCount = 0;
const urlRecord = this.routers[method] as Record>;
for (const key in urlRecord) {
if (urlRecord.hasOwnProperty(key)) {
const reg = new RegExp(key);
if (reg.test(url)) {
const callback = urlRecord[key] as RequestCallback;
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(method: RequestMethodType, url: string, callback: RequestCallback): void {
if (undefined === this.routers[method] || null === this.routers[method]) {
this.routers[method] = {} as Record>;
}
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(
`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): 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(data: T, delayCount = 1): Observable {
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 {
}