在本文中,我们首先展示一个用于缓存网络 get 请求的拦截器及其对应的缓存服务的代码,然后根据已有的代码逐渐构思我们的测试用例。测试网络通信的拦截器本身是不难的,只要抓住几个要点即可。
1. 拦截器及其缓存服务
// CacheInterceptor.ts
import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpResponse, HttpInterceptor } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { HttpCacheService } from './http-cache-service';
@Injectable()
export class CacheInterceptor implements HttpInterceptor {
constructor(private cacheService: HttpCacheService) { }
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// pass along non-cacheable requests and invalidate cache
if (req.method !== 'GET') {
console.log(
'%cInvalidating cache:',
'color:#fff;background:#008800;marign-right:12px;padding:2px 6px;display:inline-block;border-radius:4px;',
`${ req.method } ${ req.url } `,
);
this.cacheService.invalidateCache();
return next.handle(req);
}
const cachedResponse: HttpResponse<any> | undefined = this.cacheService.get(req.url);
// return cached response
if (cachedResponse) {
console.log(
'%cReturning a cached response:',
'color:#fff;background:#008800;marign-right:12px;padding:2px 6px;display:inline-block;border-radius:4px;',
`${ cachedResponse.url } `
);
return of(cachedResponse);
}
// send request to server and add response to cache
return next.handle(req).pipe(
tap(event => {
if (event instanceof HttpResponse) {
console.log(
'%cAdding item to cache:',
'color:#fff;background:#008800;marign-right:12px;padding:2px 6px;display:inline-block;border-radius:4px;',
this.cacheService.put(req.url, event)
)
}
})
);
}
}
// http-cache-service.ts
import { Injectable } from "@angular/core";
import { HttpResponse } from "@angular/common/http";
@Injectable({
providedIn: 'root'
})
export class HttpCacheService {
private requests: any = {};
constructor() { }
put(url: string, response: HttpResponse<any>): void {
this.requests[url] = response;
}
get(url: string): HttpResponse<any> | undefined {
return this.requests[url];
}
invalidateUrl(url: string): void {
this.requests[url] = undefined;
}
invalidateCache(): void {
this.requests = {};
}
}
上面的这个拦截器的作用就是每次发送网络请求的时候,对每一个请求进行检查。如果是 GET 请求,则去缓存池中查找是否有此 url 对应的数据,如果有则立刻返回,如果没有才发起 GET 请求从服务器端获取数据。
我们都知道,如果一直发送的都是 GET 请求的话,则不会改变数据库中的数据,反过来,一旦我们发出非 GET 请求,那么我们就认为相关数据一定改变,这个时候就需要清空我们的缓存池。
2. 单元测试构建思路
2.1 如何发起网络请求
如果我们要对上面的拦截器和缓存服务进行测试,首先我们需要保证能够成功的发出网络请求,而在测试环境中发送网络请求我们一般使用的是 HttpClientTestingModule, HttpTestingController;
在 configureTestingModule 中配置之后,我们还需要一个 MockComponent 并定义上面的方法,这些方法使用 HttpClient 的实例 http 发起 GET 或者 POST 请求。
2.2 如何检查缓存服务是否缓存成功
我们观察缓存服务的代码,发现我们可以通过实例方法 get 获取指定 url 的缓存内容。因此,我们只需要通过 TestBed.inject() 拿到缓存服务实例即可获取缓存的内容。
2.3 如何设计我们的测试用例
我们的测试用例最重要的是包含两个方面的内容:
- 当我们 get 请求 url 的时候,请求结束之后需要检查下面的内容(1. 成功发出了 get 请求 2. 请求回来的数据是正确的 3. 缓存池中此 url 对应有数据 4. 再次以 url get 请求的时候不会发起新的网络请求)
- 在 1 的基础之上,我们 post url2, 请求结束之后,应该检查下面的内容(1. 成功发出了 post 请求 2. 请求回来的数据是正确的 3. 缓存池被清空了 4. 再次以 url get 请求走的是网络而不是缓存)
3. 需要用到的技巧
- 如何构建成功发起网络请求的组件
@Component({
selector: 'app-test-int',
template: `<span></span>`,
})
class MockComponent {
constructor(private http: HttpClient) { }
post(url: string) {
return this.http.post(url, null);
}
get(url: string) {
return this.http.get(url);
}
}
- 如何配置测试拦截器的编译环境
TestBed.configureTestingModule({
declarations: [MockComponent],
providers: [{
provide: HTTP_INTERCEPTORS,
useClass: CacheInterceptor,
multi: true,
}],
imports: [HttpClientTestingModule],
})
- 如何获取组件示例相关
fixture = TestBed.createComponent(MockComponent);
component = fixture.componentInstance;
fixture.detectChanges();
- 如何获取缓存服务实例
service = TestBed.inject(HttpCacheService);
- 如何获取控制网络请求的实例
httpMock = TestBed.inject(HttpTestingController);
- 最先执行的测试 -- 确保编译环境的正确性
it('Should be established', () => {
expect(component).toBeTruthy();
expect(service).toBeTruthy();
})
- 发起网络请求并监视其符合预期 -- 这几乎是固定的写法
it('Should request and cache successfully', () => {
const url = 'http://www.baidu.com';
component.get(url).subscribe((data: any) => {
expect(data.data).toBe(`the result of ${url}`);
});
const req = httpMock.expectOne(url); // 不报错说明确实发起了此请求
expect(req.request.method).toBe('GET'); // 验证请求的类型是否正确
req.flush({ // mock 服务器的返回值
success: true,
data: `the result of ${url}`,
});
httpMock.verify();
})
- 如何获取缓存实例中的数据
expect(service.get(url)).toBeTruthy();
expect(service.get(url)?.body.data).toBe(`the result of ${url}`)
先保证其存在,再验证其值是否正确。
- 如何证明没有发送网络请求而是走的缓存
expect(httpMock.match(url)).toEqual([]);
4. 完整的测试代码
// cache-interceptor.spec.ts
import { HTTP_INTERCEPTORS, HttpClient } from "@angular/common/http";
import { Component } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { CacheInterceptor } from "./CacheInterceptor";
import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing";
import { HttpCacheService } from "./http-cache-service";
@Component({
selector: 'app-test-int',
template: `<span></span>`,
})
class MockComponent {
constructor(private http: HttpClient) { }
post(url: string) {
return this.http.post(url, null);
}
get(url: string) {
return this.http.get(url);
}
}
describe('Test cache interceptor', () => {
let fixture: ComponentFixture<MockComponent>;
let component: MockComponent;
let httpMock: HttpTestingController;
let service: HttpCacheService;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [MockComponent],
providers: [{
provide: HTTP_INTERCEPTORS,
useClass: CacheInterceptor,
multi: true,
}],
imports: [HttpClientTestingModule],
})
fixture = TestBed.createComponent(MockComponent);
component = fixture.componentInstance;
httpMock = TestBed.inject(HttpTestingController);
service = TestBed.inject(HttpCacheService);
fixture.detectChanges();
})
it('Should be established', () => {
expect(component).toBeTruthy();
expect(service).toBeTruthy();
})
it('Should request and cache successfully', () => {
const url = 'http://www.baidu.com';
component.get(url).subscribe((data: any) => {
expect(data.data).toBe(`the result of ${url}`);
expect(service.get(url)).toBeTruthy();
expect(service.get(url)?.body.data).toBe(`the result of ${url}`)
component.get(url).subscribe((data: any) => {
expect(data.data).toBe(`the result of ${url}`);
});
expect(httpMock.match(url)).toEqual([]);
});
const req = httpMock.expectOne(url);
expect(req.request.method).toBe('GET');
req.flush({
success: true,
data: `the result of ${url}`,
});
httpMock.verify();
})
it('Should refresh the cache successfully', () => {
const url = 'http://www.baidu.com';
component.get(url).subscribe((data: any) => {
expect(data.data).toBe(`the result of ${url}`);
component.get(url).subscribe((data: any) => {
expect(data.data).toBe(`the result of ${url}`);
const url2 = 'http://www.google.com';
component.post(url2).subscribe((data: any) => {
expect(data.data).toBe(`the result of ${url2}`);
expect(service.get(url)).toBeFalsy();
expect(service.get(url2)).toBeFalsy();
component.get(url).subscribe((data: any) => {
expect(data.data).toBe(`the result of ${url}`);
expect(service.get(url)).toBeTruthy();
expect(service.get(url)?.body.data).toBe(`the result of ${url}`)
})
const req3 = httpMock.expectOne(url);
expect(req3.request.method).toBe('GET');
req3.flush({
success: true,
data: `the result of ${url}`,
});
httpMock.verify();
});
const req2 = httpMock.expectOne(url2);
expect(req2.request.method).toBe('POST');
req2.flush({
success: true,
data: `the result of ${url2}`,
});
httpMock.verify();
});
expect(httpMock.match(url)).toEqual([]);
});
const req = httpMock.expectOne(url);
expect(req.request.method).toBe('GET');
req.flush({
success: true,
data: `the result of ${url}`,
});
httpMock.verify();
})
})
结尾
留下一个思考题:
如果我们仔细观察代码,就可以发现,在配置测试环境的时候没有显式在 configureTestingModule 中声明 HttpCacheService, 但是在之后的代码中,我们仍然可以使用
service = TestBed.inject(HttpCacheService);的到 HttpCacheService 的实例,并且这个实例正是 CacheInterceptor 被注入的那个。这一点我觉得非常的神奇!那么这是不是说明,对于服务我们无需在配置测试的编译环境的时候专门声明呢?