Preface
The previous project testing used Stub Replace the original components for testing , Today's questionnaire system adopts a new idea , It's using MockHttpClientModule Replace HttpClientModule, The front and back interfaces are completely unified , Closer to the real request , This paper mainly summarizes the learning experience , Summarize the idea of this method .
Method
Let's first look at the method to be tested :
/**
* Through the test paper ID Get answers
* 1. If there is an unfinished answer sheet , Then return to the unfinished answer sheet
* 2. If there is no unfinished answer sheet , Then create a new answer sheet to return to
* @param id The examination paper ID
*/getByPaperId(id: number): Observable<AnswerSheet> {
return this.httpClient.get<AnswerSheet>(`${this.baseUrl}/${id}`);
}
The test method :
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
MockApiModule
],
providers: [
MockHttpClient
]
});
service = TestBed.inject(AnswerSheetService);
});
it(' Test whether the simulated interface service works ', () => {
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 Method
/**
* get Method * @param url Request address
* @param options 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
/**
* be-all GETPOSTDELETEPUTPATCH Methods eventually call request Method . * If at present request Can't meet demand , Please move on angular Official HttpClient * * The method is based on method Match , Then according to URL Matching regular expressions . * After the matching is successful, the parameters are passed into the interface and the return value of the analog interface is obtained * * @param method Request method
* @param url Request address
* @param options 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(' It matches more than one URL Information , Please verify the injection service URL Information ,URL There is a matching conflict in the information ');
}
});
}
}
}
if (null === result) {
throw Error(' No corresponding analog return data was found , Please check url、method Whether it is right , Whether the simulation injection service works or not ');
}
return testingObservable(result);
}
registerMockApi
/**
* Register the analog interface * @param url Request address
* @param method Request method
* @param callback 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(` At the address ${url} Already exists ${method} Route records for `);
}
this.routers[method][url] = callback;
}
AnswerSheetApi
registerGetByPaperId()
private baseUrl = '/answerSheet';
/**
* register GetByPaperId Interface
* After registration , When other services try httpClient when
* The registration method will be used at this time 、URL Address matching
* After the match is successful, the callback function declared here will be called , The address will also be requested 、 Request parameters 、 request header The message is coming
* We finally return specific analog data based on the received parameters , The data is strictly consistent with the real interface in the background */
registerGetByPaperId(): void {
this.mockHttpService.registerMockApi<AnswerSheet>(
`get`,
`^${this.baseUrl}/(d+)$`,
(urlMatches, httpParams, httpHeaders, callback) => {
const id = urlMatches[1];
callback({
id: +id
});
}
);
}
injectMockHttpService
/**
* MOCK service .
*/mockHttpService: MockApiService;
/**
* Inject MOCK service
** @param mockHttpService simulation HTTP service
*/
injectMockHttpService(mockHttpService: MockApiService): void {
this.mockHttpService = mockHttpService;
this.registerGetByPaperId();
}
MockApiService
constructor()
/**
* Register the analog interface
* @param clazz Interface type
*/
static registerMockApi(clazz: Type<Api>): void {
this.mockApiRegisters.push(clazz);
}
/**
* Loop call to complete all interface registration */
constructor() {
MockApiService.mockApiRegisters.forEach(api => {
const instance = new api();
instance.injectMockHttpService(this);
});
}
// AnswerSheetApi
MockApiService.registerMockApi(AnswerSheetApi);
testingObservable
/**
* Return to the observer for testing
* If it is currently in the process of testing , Call cold Method returns the observer without throwing an exception .
* Otherwise use of Method back to the observer
* @param data Returned data
* @param delayCount Delay return interval
*/
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
/**
* Simulation background interface module
* because MockHttpClient Depend on MockApiService
* So it must be stated first that MockApiService, And then state MockHttpClient
* Otherwise, a dependency exception will be generated
* Every time you add a background analog interface , You need to add the providers.
* Otherwise, the analog interface will be angular Shake the tree optimization shake off , So that its registration method fails
*/
@NgModule({
providers: [
MockApiService,
{provide: HttpClient, useClass: MockHttpClient},
AnswerSheetApi
]
})
export class MockApiModule {
}