如何在Jasmine中测试服务map()处理的JSON响应并解决测试报错
问题分析
你遇到的Error: Cannot make XHRs from within a fake async test是因为**MockBackend和fakeAsync不能混用**——MockBackend本身是同步模拟HTTP请求的,而fakeAsync会拦截异步操作(包括XHR),导致冲突。另外你最初的测试代码没有订阅Observable,也没有验证任何业务逻辑,自然无法有效测试。
接下来给你两种可行的测试方案,优先推荐官方现在主推的HttpClientTestingModule方案,再给你修复MockBackend的写法。
方案一:使用官方推荐的HttpClientTestingModule(推荐)
从Angular 4.3开始,官方推出了HttpClientTestingModule来简化HTTP服务的测试,比MockBackend更直观易用。
步骤1:配置测试模块
首先在TestBed中导入HttpClientTestingModule,并mock掉AuthService(因为我们需要验证logout方法是否被调用):
import { TestBed, inject } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { AlarmsService } from './alarms.service'; import { AuthService } from './auth.service'; import { Alarm } from './alarm.model'; import { Api } from './api.constants'; describe('AlarmsService', () => { let service: AlarmsService; let httpMock: HttpTestingController; let mockAuthService: jasmine.SpyObj<AuthService>; beforeEach(() => { // 创建AuthService的spy对象,用于验证logout调用 mockAuthService = jasmine.createSpyObj('AuthService', ['logout']); TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [ AlarmsService, { provide: AuthService, useValue: mockAuthService } ] }); service = TestBed.inject(AlarmsService); httpMock = TestBed.inject(HttpTestingController); }); // 每次测试后验证所有HTTP请求都被处理 afterEach(() => { httpMock.verify(); });
步骤2:测试正常返回数据的场景(StatusCode≠1)
it('should map StatusDescription to Alarm instances when StatusCode is not 1', () => { const mockResponse = { StatusCode: 0, StatusDescription: [ { alarmnumber: 0, alarmdesc: 'All cats are lions' }, { alarmnumber: 1, alarmdesc: 'Video 1' } ] }; service.typegetall().subscribe(alarms => { expect(alarms.length).toBe(2); // 验证返回的是Alarm实例 expect(alarms[0] instanceof Alarm).toBeTruthy(); expect(alarms[0].alarmdesc).toEqual('All cats are lions'); // 验证auth.logout没有被调用 expect(mockAuthService.logout).not.toHaveBeenCalled(); }); // 捕获并响应HTTP请求 const req = httpMock.expectOne(Api.getUrl(Api.URLS.typegetall)); expect(req.request.method).toBe('GET'); req.flush(mockResponse); });
步骤3:测试StatusCode=1的场景(调用auth.logout)
it('should call auth.logout when StatusCode is 1', () => { const mockResponse = { StatusCode: 1, StatusDescription: [] }; service.typegetall().subscribe(alarms => { // 此时分支没有return,所以返回undefined expect(alarms).toBeUndefined(); // 验证auth.logout被调用 expect(mockAuthService.logout).toHaveBeenCalled(); }); const req = httpMock.expectOne(Api.getUrl(Api.URLS.typegetall)); req.flush(mockResponse); }); });
方案二:修复MockBackend的报错问题
如果你坚持使用MockBackend,需要移除fakeAsync和tick(),因为MockBackend的响应是同步的,不需要fakeAsync来处理异步:
import { TestBed, inject } from '@angular/core/testing'; import { AlarmsService } from './alarms.service'; import { AuthService } from './auth.service'; import { MockBackend, MockConnection } from '@angular/http/testing'; import { Http, BaseRequestOptions, Response, ResponseOptions } from '@angular/http'; import { Alarm } from './alarm.model'; import { Api } from './api.constants'; describe('AlarmsService', () => { beforeEach(() => { const mockAuthService = jasmine.createSpyObj('AuthService', ['logout']); TestBed.configureTestingModule({ providers: [ AlarmsService, MockBackend, BaseRequestOptions, { provide: AuthService, useValue: mockAuthService }, { provide: Http, useFactory: (backend: MockBackend, options: BaseRequestOptions) => new Http(backend, options), deps: [MockBackend, BaseRequestOptions] } ] }); }); it('should return Alarm instances when StatusCode is not 1', inject( [AlarmsService, MockBackend, AuthService], (service: AlarmsService, mockBackend: MockBackend, authService: AuthService) => { const mockResponse = { StatusCode: 0, StatusDescription: [ { alarmnumber: 0, alarmdesc: 'All cats are lions' }, { alarmnumber: 1, alarmdesc: 'Video 1' } ] }; mockBackend.connections.subscribe((connection: MockConnection) => { connection.mockRespond(new Response(new ResponseOptions({ body: mockResponse }))); }); service.typegetall().subscribe(alarms => { expect(alarms.length).toBe(2); expect(alarms[0] instanceof Alarm).toBeTruthy(); expect(alarms[0].alarmdesc).toEqual('All cats are lions'); expect(authService.logout).not.toHaveBeenCalled(); }); } )); it('should call auth.logout when StatusCode is 1', inject( [AlarmsService, MockBackend, AuthService], (service: AlarmsService, mockBackend: MockBackend, authService: AuthService) => { const mockResponse = { StatusCode: 1, StatusDescription: [] }; mockBackend.connections.subscribe((connection: MockConnection) => { connection.mockRespond(new Response(new ResponseOptions({ body: mockResponse }))); }); service.typegetall().subscribe(alarms => { expect(alarms).toBeUndefined(); expect(authService.logout).toHaveBeenCalled(); }); } )); });
关键注意点
- 最初测试无效的原因:你只是
expect(alarmeservice.typegetall());,但typegetall()返回的是Observable,必须订阅它才能触发内部逻辑,否则代码根本不会执行到HTTP请求和map操作符里的逻辑。 - 报错的根本原因:
fakeAsync会拦截所有异步操作(包括XHR),而MockBackend是模拟的XHR,两者冲突,所以移除fakeAsync和tick()即可解决。 - 优先使用HttpClientTestingModule:
MockBackend是针对老的Http服务的,现在Angular推荐使用HttpClient,对应的测试工具就是HttpClientTestingModule,API更简洁,维护性更好。
内容的提问来源于stack exchange,提问作者user9699663
相关产品推荐
相关产品推荐

