单元测试 Angular 12 HTTP 拦截器 expectOne 失败
Unit Testing Angular 12 HTTP Interceptor expectOne fails
我有一个使用 Firebase 进行身份验证的 Angular 项目。为了向我们的服务器进行身份验证,我们需要来自 Firebase 的 JWT。该项目正在使用 Angular Fire 来完成此任务。
我们有一个 HTTP 拦截器,可以拦截对服务器的任何请求,并添加所需的授权 header。拦截器似乎工作完美,但我们也想对其进行单元测试。
我们的 AuthService 负责将操作委托给 Firebase。 getCurrentUserToken
方法检索 JWT 作为承诺,因为 Angular Fire 将在需要时刷新 JWT。
这会使拦截器稍微复杂一些,因为我们需要异步获取令牌并将其包装到来自请求的 Observable 中。
token-interceptor.ts
@Injectable()
export class TokenInterceptor implements HttpInterceptor {
constructor(private authService: AuthService) {}
intercept(
req: HttpRequest<unknown>,
next: HttpHandler
): Observable<HttpEvent<unknown>> {
if (req.url.includes(environment.apiRoot)) {
return defer(async () => {
const token = await this.authService.getCurrentUserToken();
const request = req.clone({
headers: this.getDefaultHeaders(token),
withCredentials: true,
});
return next.handle(request);
}).pipe(mergeAll());
} else {
return next.handle(req);
}
}
private getDefaultHeaders(accessToken: string): HttpHeaders {
return new HttpHeaders()
.set(
'Access-Control-Allow-Origin',
`${window.location.protocol}//${window.location.host}`
)
.set(
'Access-Control-Allow-Headers',
'Access-Control-Allow-Origin,Access-Control-Request-Method,Access-Control-Request-Headers,Access-Control-Allow-Headers,Authorization,Accept,Content-Type,Origin,Host,Referer,X-Requested-With,X-CSRF-Token'
)
.set('Content-Type', 'application/json')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${accessToken}`);
}
}
我们通过导入 HttpClientModule
并在 HTTP_INTERCEPTORS
注入令牌下提供带有
的拦截器,从而在我们的 AppModule 中提供拦截器
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: TokenInterceptor,
multi: true,
},
问题只出现在我们的单元测试中
token-interceptor.spec.ts
describe(`TokenInterceptor`, () => {
let service: SomeService;
let httpMock: HttpTestingController;
let authServiceStub: AuthService;
beforeEach(() => {
authServiceStub = {
getCurrentUserToken: () => Promise.resolve('some-token'),
} as AuthService;
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: TokenInterceptor,
multi: true,
},
{ provide: AuthService, useValue: authServiceStub },
],
});
service = TestBed.inject(SomeService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
it('should add an Authorization header', () => {
service.sendSomeRequestToServer().subscribe((response) => {
expect(response).toBeTruthy();
});
const httpRequest = httpMock.expectOne({});
expect(httpRequest.request.headers.has('Authorization')).toEqual(true);
expect(httpRequest.request.headers.get('Authorization')).toBe(
'Bearer some-token'
);
httpRequest.flush({ message: "SUCCESS" });
httpMock.verify();
});
});
测试在 httpMock.expectOne({})
语句处失败并显示消息
Error: Expected one matching request for criteria "Match method: (any), URL: (any)", found none.
似乎我们的模拟请求根本没有发送过。
但是,如果我们从 Testbed 中的提供程序中删除拦截器,测试反而会在 expect 语句处失败,这表明正在找到模拟请求。
Error: Expected false to equal true.
at <Jasmine>
at UserContext.<anonymous> (src/app/http-interceptors/token-interceptor.spec.ts:57:62)
at ZoneDelegate.invoke (node_modules/zone.js/fesm2015/zone.js:372:1)
at ProxyZoneSpec.onInvoke (node_modules/zone.js/fesm2015/zone-testing.js:287:1)
Error: Expected null to be 'Bearer some-token'.
at <Jasmine>
at UserContext.<anonymous> (src/app/http-interceptors/token-interceptor.spec.ts:58:62)
at ZoneDelegate.invoke (node_modules/zone.js/fesm2015/zone.js:372:1)
at ProxyZoneSpec.onInvoke (node_modules/zone.js/fesm2015/zone-testing.js:287:1)
为什么 expectOne 找不到我们服务发送的请求?问题出在拦截器本身还是测试的构建方式?
我认为问题出在拦截器中,因为在继续断言之前我们可能没有等待 const token = await this.authService.getCurrentUserToken();
的 async
任务完成。
在拦截器中添加一个这样的console.log:
return defer(async () => {
console.log('[Interceptor] Before token');
const token = await this.authService.getCurrentUserToken();
console.log('[Interceptor] After token');
const request = req.clone({
headers: this.getDefaultHeaders(token),
withCredentials: true,
});
return next.handle(request);
}).pipe(mergeAll());
尝试使用 fakeAsync 并勾选:
it('should add an Authorization header', fakeAsync(() => {
service.sendSomeRequestToServer().subscribe((response) => {
expect(response).toBeTruthy();
});
// Add a tick here to tell Angular to finish all promises encountered
// in the code before carrying forward with the tests.
tick();
console.log('[Test] Before expectation');
const httpRequest = httpMock.expectOne({});
expect(httpRequest.request.headers.has('Authorization')).toEqual(true);
expect(httpRequest.request.headers.get('Authorization')).toBe(
'Bearer some-token'
);
httpRequest.flush({ message: "SUCCESS" });
httpMock.verify();
}));
如果没有 tick
,我怀疑您会看到 [Interceptor] Before token
,然后是 [Test] Before expectation
(错误的顺序),而使用勾号后您应该会看到 [Interceptor] Before token
、[Interceptor] After token
然后 [Test] Before expectation
(正确的顺序)
我有一个使用 Firebase 进行身份验证的 Angular 项目。为了向我们的服务器进行身份验证,我们需要来自 Firebase 的 JWT。该项目正在使用 Angular Fire 来完成此任务。
我们有一个 HTTP 拦截器,可以拦截对服务器的任何请求,并添加所需的授权 header。拦截器似乎工作完美,但我们也想对其进行单元测试。
我们的 AuthService 负责将操作委托给 Firebase。 getCurrentUserToken
方法检索 JWT 作为承诺,因为 Angular Fire 将在需要时刷新 JWT。
这会使拦截器稍微复杂一些,因为我们需要异步获取令牌并将其包装到来自请求的 Observable 中。
token-interceptor.ts
@Injectable()
export class TokenInterceptor implements HttpInterceptor {
constructor(private authService: AuthService) {}
intercept(
req: HttpRequest<unknown>,
next: HttpHandler
): Observable<HttpEvent<unknown>> {
if (req.url.includes(environment.apiRoot)) {
return defer(async () => {
const token = await this.authService.getCurrentUserToken();
const request = req.clone({
headers: this.getDefaultHeaders(token),
withCredentials: true,
});
return next.handle(request);
}).pipe(mergeAll());
} else {
return next.handle(req);
}
}
private getDefaultHeaders(accessToken: string): HttpHeaders {
return new HttpHeaders()
.set(
'Access-Control-Allow-Origin',
`${window.location.protocol}//${window.location.host}`
)
.set(
'Access-Control-Allow-Headers',
'Access-Control-Allow-Origin,Access-Control-Request-Method,Access-Control-Request-Headers,Access-Control-Allow-Headers,Authorization,Accept,Content-Type,Origin,Host,Referer,X-Requested-With,X-CSRF-Token'
)
.set('Content-Type', 'application/json')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${accessToken}`);
}
}
我们通过导入 HttpClientModule
并在 HTTP_INTERCEPTORS
注入令牌下提供带有
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: TokenInterceptor,
multi: true,
},
问题只出现在我们的单元测试中
token-interceptor.spec.ts
describe(`TokenInterceptor`, () => {
let service: SomeService;
let httpMock: HttpTestingController;
let authServiceStub: AuthService;
beforeEach(() => {
authServiceStub = {
getCurrentUserToken: () => Promise.resolve('some-token'),
} as AuthService;
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: TokenInterceptor,
multi: true,
},
{ provide: AuthService, useValue: authServiceStub },
],
});
service = TestBed.inject(SomeService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
it('should add an Authorization header', () => {
service.sendSomeRequestToServer().subscribe((response) => {
expect(response).toBeTruthy();
});
const httpRequest = httpMock.expectOne({});
expect(httpRequest.request.headers.has('Authorization')).toEqual(true);
expect(httpRequest.request.headers.get('Authorization')).toBe(
'Bearer some-token'
);
httpRequest.flush({ message: "SUCCESS" });
httpMock.verify();
});
});
测试在 httpMock.expectOne({})
语句处失败并显示消息
Error: Expected one matching request for criteria "Match method: (any), URL: (any)", found none.
似乎我们的模拟请求根本没有发送过。
但是,如果我们从 Testbed 中的提供程序中删除拦截器,测试反而会在 expect 语句处失败,这表明正在找到模拟请求。
Error: Expected false to equal true.
at <Jasmine>
at UserContext.<anonymous> (src/app/http-interceptors/token-interceptor.spec.ts:57:62)
at ZoneDelegate.invoke (node_modules/zone.js/fesm2015/zone.js:372:1)
at ProxyZoneSpec.onInvoke (node_modules/zone.js/fesm2015/zone-testing.js:287:1)
Error: Expected null to be 'Bearer some-token'.
at <Jasmine>
at UserContext.<anonymous> (src/app/http-interceptors/token-interceptor.spec.ts:58:62)
at ZoneDelegate.invoke (node_modules/zone.js/fesm2015/zone.js:372:1)
at ProxyZoneSpec.onInvoke (node_modules/zone.js/fesm2015/zone-testing.js:287:1)
为什么 expectOne 找不到我们服务发送的请求?问题出在拦截器本身还是测试的构建方式?
我认为问题出在拦截器中,因为在继续断言之前我们可能没有等待 const token = await this.authService.getCurrentUserToken();
的 async
任务完成。
在拦截器中添加一个这样的console.log:
return defer(async () => {
console.log('[Interceptor] Before token');
const token = await this.authService.getCurrentUserToken();
console.log('[Interceptor] After token');
const request = req.clone({
headers: this.getDefaultHeaders(token),
withCredentials: true,
});
return next.handle(request);
}).pipe(mergeAll());
尝试使用 fakeAsync 并勾选:
it('should add an Authorization header', fakeAsync(() => {
service.sendSomeRequestToServer().subscribe((response) => {
expect(response).toBeTruthy();
});
// Add a tick here to tell Angular to finish all promises encountered
// in the code before carrying forward with the tests.
tick();
console.log('[Test] Before expectation');
const httpRequest = httpMock.expectOne({});
expect(httpRequest.request.headers.has('Authorization')).toEqual(true);
expect(httpRequest.request.headers.get('Authorization')).toBe(
'Bearer some-token'
);
httpRequest.flush({ message: "SUCCESS" });
httpMock.verify();
}));
如果没有 tick
,我怀疑您会看到 [Interceptor] Before token
,然后是 [Test] Before expectation
(错误的顺序),而使用勾号后您应该会看到 [Interceptor] Before token
、[Interceptor] After token
然后 [Test] Before expectation
(正确的顺序)