Jasmine toHaveBeenCalledWith参数匹配时机及Angular测试问题咨询
这个问题我之前也碰到过!其实不是Jasmine的toHaveBeenCalledWith逻辑反常,核心是JavaScript里对象的引用传递特性在搞鬼,咱们一步步拆解清楚:
先还原你的场景代码
假设你的Angular服务和测试代码大概是这样:
服务代码
@Injectable() export class MyService { f1() { // 声明一个对象类型的变量v2 const v2 = { a: 1 }; // 调用f2并传入v2 this.f2(v2); } f2(v2: { a: number }) { // 修改传入对象的属性 v2.a = 3; } }
测试代码
describe('MyService', () => { let service: MyService; beforeEach(() => { TestBed.configureTestingModule({ providers: [MyService] }); service = TestBed.inject(MyService); // 对f2创建间谍监听 spyOn(service, 'f2'); }); it('should call f2 with original v2', () => { service.f1(); // 预期f2被调用时传入的是{a:1},但断言失败 expect(service.f2).toHaveBeenCalledWith({ a: 1 }); }); });
为什么断言会失败?
因为v2是引用类型(对象),当你把它传给f2时,传递的不是对象的副本,而是指向内存中同一个对象的引用。
所以当f2执行v2.a = 3时,它修改的是内存中那个唯一的对象——等到你在测试里执行toHaveBeenCalledWith断言时,这个对象的a字段已经变成3了。Jasmine的toHaveBeenCalledWith会对比参数的当前状态,自然就和你预期的{a:1}不匹配了。
如果你的测试改成断言传入的是修改后的对象,反而会通过:
expect(service.f2).toHaveBeenCalledWith({ a: 3 }); // 这个会成功
解决方案:针对引用类型的测试优化
根据你的需求,有几种可行的解决思路:
1. 遵循Immutable原则,不修改原对象(推荐)
修改f2,让它返回一个新对象而不是修改传入的对象:
f2(v2: { a: number }) { // 解构原对象,创建新对象并修改a字段 return { ...v2, a: 3 }; }
这样f1里如果需要修改后的对象,可以接收返回值:
f1() { const v2 = { a: 1 }; const updatedV2 = this.f2(v2); // 此时v2还是{a:1},updatedV2是{a:3} }
这种情况下,你的原始测试断言toHaveBeenCalledWith({a:1})就会成功,因为原对象没有被修改。
2. 在测试中捕获调用时的原始参数状态
如果不能修改服务代码,你可以通过间谍的callFake方法,在f2修改对象前捕获原始参数的快照:
it('should call f2 with original v2', () => { let originalV2Snapshot: { a: number }; // 替换f2的实现,先保存原始参数再执行原逻辑 spyOn(service, 'f2').and.callFake((v) => { // 深拷贝原始参数,保存快照 originalV2Snapshot = JSON.parse(JSON.stringify(v)); // 执行f2的原方法 return (service.f2 as jasmine.Spy).calls.mostRecent().returnValue = service.f2(v); }); service.f1(); // 断言原始参数状态符合预期 expect(originalV2Snapshot).toEqual({ a: 1 }); // 同时确认f2确实被调用过 expect(service.f2).toHaveBeenCalled(); });
3. 对比引用而非对象内容
如果你只需要确认f2被调用时传入的是f1里创建的那个对象(不管属性有没有被修改),可以修改f1返回v2,然后测试引用一致性:
// 修改服务的f1方法 f1() { const v2 = { a: 1 }; this.f2(v2); return v2; } // 测试代码 it('should call f2 with the v2 created in f1', () => { const v2FromF1 = service.f1(); // 对比引用是否一致 expect(service.f2).toHaveBeenCalledWith(v2FromF1); });
这个断言会成功,因为v2FromF1和传给f2的是同一个对象(引用相同),哪怕它的属性已经被修改。
总结
Jasmine的toHaveBeenCalledWith逻辑是符合预期的:它会在你执行断言时,检查参数的当前状态。对于引用类型的对象,因为传递的是引用,对象属性被修改后,参数的状态自然也会变化。问题的核心是对JavaScript引用类型传递机制的理解,而不是Jasmine的问题。
内容的提问来源于stack exchange,提问作者Harsh Shah




