Angular单元测试使用useValue模拟服务时出现‘is not a function’错误
问题根源
你碰到的问题本质很简单:当你用useValue来模拟AdminService时,你只给了avcData这一个属性,但你的组件在初始化过程中调用了adminService.toLocalDate()方法——而你的模拟对象里根本没有定义这个方法,所以才会抛出TypeError: _this.adminService.toLocalDate is not a function的错误。
useValue的作用是直接用你提供的对象替换掉真实的服务,它不会自动继承原服务的任何方法或属性,所以你需要手动把组件用到的所有成员都补全。
解决方案
这里有几种不同的处理方式,你可以根据自己的测试场景选择:
1. 手动在模拟对象中添加toLocalDate方法
直接在useValue的对象里补上这个方法,实现可以根据测试需求写最简单的逻辑:
providers: [ { provide: AdminService, useValue: { avcData: { runMode: 'NORMAL' }, // 模拟toLocalDate方法,返回符合测试预期的值 toLocalDate: (inputDate: Date | string) => { // 比如返回固定格式的日期,或者简单处理输入 return new Date(inputDate).toLocaleDateString('zh-CN'); } } } ]
如果你的测试不需要关心方法的具体逻辑,甚至可以直接返回一个固定值,比如return '2024-05-20',只要能让组件正常初始化就行。
2. 用jasmine.createSpyObj创建带Spy的模拟对象(推荐)
如果你的测试框架是Jasmine,这种方式更灵活,不仅能解决方法不存在的问题,还能后续验证方法的调用情况:
beforeEach(async () => { // 创建Spy对象:第一个参数是服务名称,第二个是要模拟的方法数组,第三个是静态属性 const adminServiceSpy = jasmine.createSpyObj( 'AdminService', ['toLocalDate'], { avcData: { runMode: 'NORMAL' } } ); // 可以提前设置方法的返回值 adminServiceSpy.toLocalDate.and.returnValue('2024-05-20'); await TestBed.configureTestingModule({ declarations: [YourComponentName], providers: [ { provide: AdminService, useValue: adminServiceSpy } ] }).compileComponents(); });
之后你还能在测试用例里验证这个方法是否被调用:
it('should call toLocalDate when initialized', () => { const adminService = TestBed.inject(AdminService) as jasmine.SpyObj<AdminService>; expect(adminService.toLocalDate).toHaveBeenCalled(); });
3. 直接复用真实服务的方法(谨慎使用)
如果toLocalDate是一个纯工具方法,没有依赖服务实例的其他属性,你可以直接把原服务原型上的方法挂到模拟对象上:
import { AdminService } from './path-to-admin.service'; // ... providers: [ { provide: AdminService, useValue: { avcData: { runMode: 'NORMAL' }, toLocalDate: AdminService.prototype.toLocalDate } } ]
注意:如果toLocalDate方法内部用到了this(比如访问this.avcData或者其他服务属性),这种方式可能会因为上下文不对而出错,所以只有当方法完全独立时才推荐用。
总结
核心原则就是:你用useValue提供的模拟对象,必须包含组件在运行过程中会访问到的所有属性和方法。原服务的方法不会自动被继承,所以一定要把需要的成员都补全,这样组件初始化时就不会再抛出方法不存在的错误了。
内容的提问来源于stack exchange,提问作者TheUnreal

