You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Jasmine toHaveBeenCalledWith参数匹配时机及Angular测试问题咨询

问题根源:JavaScript引用类型的传递特性

这个问题我之前也碰到过!其实不是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

火山引擎 最新活动