如何测试依赖Http的Angular CityService?解决注入错误
解决Angular CityService测试的依赖注入问题
你遇到的NullInjectorError: No provider for Http!错误,本质是测试模块没有为CityService的依赖项(Http、Router、AuthService)提供对应的服务实例。在Angular单元测试中,我们通常不会使用真实的HTTP服务或路由服务,而是通过模拟依赖或者使用Angular提供的测试模块来隔离测试目标。
1. 准备模拟依赖
首先,我们需要为AuthService和Router创建模拟类——我们不需要测试这些依赖的功能,只需要模拟它们的核心行为来支撑CityService的测试:
// 模拟AuthService,实现原服务的核心方法 class MockAuthService { getCurrentUser() { return { token: 'test-demo-token' }; // 返回测试用的token } logout() {} // 空方法,后续用间谍函数监控调用情况 } // 模拟Router,原CityService仅注入未使用,所以留空即可 class MockRouter {}
2. 配置测试模块
在beforeEach中,我们需要导入Angular官方的HttpClientTestingModule(用于模拟HTTP请求),并将模拟的依赖和CityService注册到测试模块中:
import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { CityService } from './city.service'; import { City } from './city.model'; // 导入你的City模型类 import { AuthService } from './auth.service'; import { Router } from '@angular/router'; import { Api } from './api.config'; // 导入你的Api配置类 describe('Service: City', () => { let cityService: CityService; let httpTestingController: HttpTestingController; let authService: MockAuthService; beforeEach(() => { authService = new MockAuthService(); TestBed.configureTestingModule({ imports: [HttpClientTestingModule], // 导入HTTP测试模块,模拟Http服务 providers: [ CityService, { provide: AuthService, useValue: authService }, // 替换为模拟的AuthService { provide: Router, useClass: MockRouter } // 替换为模拟的Router ] }); // 获取服务实例 cityService = TestBed.inject(CityService); httpTestingController = TestBed.inject(HttpTestingController); }); // 测试完成后验证所有HTTP请求都已处理,避免遗漏 afterEach(() => { httpTestingController.verify(); });
3. 编写测试用例
现在可以针对getAllCity的两种核心场景编写测试:
测试成功返回城市列表
it('#getAllCity should return an Observable of City[] with correct data', () => { const mockCityData = [ { id: 1, name: 'Beijing' }, { id: 2, name: 'Shanghai' } ]; cityService.getAllCity().subscribe(cities => { expect(cities.length).toBe(2); expect(cities[0].name).toBe('Beijing'); expect(cities.every(city => city instanceof City)).toBe(true); }); // 捕获目标HTTP请求 const req = httpTestingController.expectOne(Api.getUrl(Api.URLS.getAllCity)); // 验证请求方法和请求头 expect(req.request.method).toBe('GET'); expect(req.request.headers.get('x-access-token')).toBe('test-demo-token'); // 返回模拟的成功响应 req.flush({ StatusCode: 0, // 假设0代表请求成功 StatusDescription: mockCityData }); });
测试StatusCode为1时触发登出
it('#getAllCity should call auth.logout when StatusCode is 1', () => { // 为logout方法创建间谍,监控是否被调用 spyOn(authService, 'logout'); cityService.getAllCity().subscribe({ complete: () => { expect(authService.logout).toHaveBeenCalled(); } }); const req = httpTestingController.expectOne(Api.getUrl(Api.URLS.getAllCity)); // 返回模拟的异常响应 req.flush({ StatusCode: 1, StatusDescription: [] }); });
4. 完整测试代码示例
把以上部分整合起来,就是完整可运行的测试代码:
import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { CityService } from './city.service'; import { City } from './city.model'; import { AuthService } from './auth.service'; import { Router } from '@angular/router'; import { Api } from './api.config'; // 模拟AuthService class MockAuthService { getCurrentUser() { return { token: 'test-demo-token' }; } logout() {} } // 模拟Router class MockRouter {} describe('Service: City', () => { let cityService: CityService; let httpTestingController: HttpTestingController; let authService: MockAuthService; beforeEach(() => { authService = new MockAuthService(); TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [ CityService, { provide: AuthService, useValue: authService }, { provide: Router, useClass: MockRouter } ] }); cityService = TestBed.inject(CityService); httpTestingController = TestBed.inject(HttpTestingController); }); afterEach(() => { httpTestingController.verify(); }); it('#getAllCity should return an Observable of City[] with correct data', () => { const mockCityData = [ { id: 1, name: 'Beijing' }, { id: 2, name: 'Shanghai' } ]; cityService.getAllCity().subscribe(cities => { expect(cities.length).toBe(2); expect(cities[0].id).toBe(1); expect(cities[1].name).toBe('Shanghai'); expect(cities.every(city => city instanceof City)).toBe(true); }); const req = httpTestingController.expectOne(Api.getUrl(Api.URLS.getAllCity)); expect(req.request.method).toBe('GET'); expect(req.request.headers.get('x-access-token')).toBe('test-demo-token'); req.flush({ StatusCode: 0, StatusDescription: mockCityData }); }); it('#getAllCity should trigger auth.logout when StatusCode is 1', () => { spyOn(authService, 'logout'); cityService.getAllCity().subscribe({ complete: () => { expect(authService.logout).toHaveBeenCalled(); } }); const req = httpTestingController.expectOne(Api.getUrl(Api.URLS.getAllCity)); req.flush({ StatusCode: 1, StatusDescription: [] }); }); });
关键要点总结
- 对于HTTP服务,使用
HttpClientTestingModule和HttpTestingController模拟请求与响应,避免真实网络请求影响测试稳定性。 - 对于其他依赖服务,通过模拟类或间谍函数隔离测试目标,只关注
CityService自身的业务逻辑。 - 测试完成后用
httpTestingController.verify()确保所有HTTP请求都被处理,避免遗漏未捕获的请求。
内容的提问来源于stack exchange,提问作者user9699663
相关产品推荐
相关产品推荐

