- 测试
- 开发工具
【免费下载链接】sinon
Test spies, stubs and mocks for JavaScript.
spyCall.firstArg是 Sinon 中 spy call 对象的一个核心只读属性,用于获取某一次函数调用传入的第一个参数值。它适用于 fake、spy、stub 和 mock 方法等所有被 Sinon 代理过的函数,是细粒度验证调用行为、提取回调参数时最常用的入口之一。读完本文,你将掌握firstArg的语义边界(含无参数调用时的表现)、获取 spyCall 的多种途径,以及它在源码中的实现原理。
spyCall.firstArg是什么
在 Sinon 的概念体系中,spy call 是对一次“被监视函数调用”的对象化表示。当 fake、spy、stub 或 mock 方法 被调用时,Sinon 会为每一次调用生成一个独立的 spyCall 对象,其中就包含本次调用的全部细节:
args:本次调用的参数数组firstArg:本次调用的第一个参数(本文主题)lastArg:本次调用的最后一个参数callback:本次调用中作为回调的函数参数returnValue:本次调用的返回值exception:本次调用抛出的异常thisValue:本次调用执行时的this上下文
firstArg的官方定义非常简洁——它保存了对该次调用第一个参数的引用(参见 first-arg.md)。这里的“引用”一词值得注意:它直接指向原始参数值本身,而非拷贝,因此对复杂对象(如对象、数组、函数)使用时,比较的是引用与原始值。
获取 spyCall 对象的五种途径
要读取firstArg,首先需要拿到某个 spyCall 对象。Sinon 提供了以下常用途径:
1.getCall(n):按索引取第 n 次调用
fake.getCall(n)返回第n次调用的 spyCall(索引从 0 开始)。在 proxy.js 的实现中,getCall还支持负数索引——-1表示从最后一次调用往前数:
const f = sinon.fake(); f("a"); f("b"); f("c"); f.getCall(0).firstArg; // "a" f.getCall(1).firstArg; // "b" f.getCall(-1).firstArg; // "c"(倒数第一次调用)2.firstCall/lastCall:首尾调用快捷属性
proxy-call-util.js 中的createCallProperties会在每次调用后更新代理上的firstCall、secondCall、thirdCall与lastCall属性:
const f = sinon.fake(); f("apple pie", "banana pie"); f.lastCall.firstArg; // "apple pie" f.firstCall.firstArg; // "apple pie"3.getCalls():批量获取全部调用
当需要遍历所有调用逐个检查firstArg时,getCalls()返回所有 spyCall 的数组。
4.secondCall/thirdCall:特定序号调用
与firstCall类似,secondCall与thirdCall分别指向第二、第三次调用(不存在时为null)。
5.this.args[0]:从代理级参数数组取首参
代理对象本身也有args(所有调用的参数数组的数组),f.args[0][0]等价于f.getCall(0).firstArg。但 spyCall 的firstArg语义更直观,且不受args二维结构影响。
基本行为:有参调用与无参调用
firstArg的取值规则由创建 spyCall 时的参数个数决定。参照 first-arg.test.js 中的官方测试:
import t from "tap"; import sinon from "sinon"; const f = sinon.fake(); // 有参数调用:firstArg 为第一个参数 f("apple pie", "banana pie"); t.equal(f.lastCall.firstArg, "apple pie"); // 通过 // 无参数调用:firstArg 为 undefined f(); t.equal(f.lastCall.firstArg, undefined); // 通过两个关键结论:
- 有参数时,
firstArg精确等于args[0]; - 无参数调用时,
firstArg为undefined,不会抛出异常或返回null。
这一点在 fakes 基础用法测试 中也有印证:未传参数的 fake 调用后,fake.firstArg为undefined。因此,断言firstArg是否为undefined可以用来判断某次调用是否携带了参数。
结合其他 spyCall 属性完成组合断言
firstArg常常需要与args、lastArg等属性配合使用,才能准确描述一次调用:
const spy = sinon.spy(); spy("query", { page: 2 }, callback); const call = spy.lastCall; call.firstArg; // "query",首个参数 call.args; // ["query", { page: 2 }, callback],完整参数数组 call.args[0]; // 等价于 call.firstArg call.lastArg; // callback(最后一个参数)特别地,lastArg与callback有关联:在 proxy-call.js 的创建逻辑中,若最后一个参数是函数,它同时会成为lastArg和callback。结合 callback 属性文档 可进一步提取回调。
在 fake、spy、stub 与 mock 上的统一表现
由于 fake、spy、stub 与 mock 方法最终都经由同一套 proxy 机制创建调用记录(proxy.js、proxy-invoke.js),firstArg的行为在四种对象上完全一致:
// spy:包装现有函数 const spy = sinon.spy((name) => `hello ${name}`); spy("world"); spy.lastCall.firstArg; // "world" // stub:预定义行为 const stub = sinon.stub().returns(1); stub("x", "y"); stub.lastCall.firstArg; // "x" // fake:独立假函数 const fake = sinon.fake.returns(42); fake(1, 2); fake.lastCall.firstArg; // 1 // mock 方法 const obj = { greet: () => {} }; const mock = sinon.mock(obj); const expectation = mock.expects("greet"); obj.greet("hi"); expectation.lastCall.firstArg; // "hi"(expectation 本身也是代理)源码级原理:firstArg从何而来
理解firstArg的底层实现,能帮你预判各种边界情况。核心逻辑位于 proxy-call.js 的createProxyCall工厂函数:
export default function createProxyCall( proxy, thisValue, args, returnValue, exception, id, errorWithCallStack, ) { let firstArg, lastArg; if (args.length > 0) { firstArg = args[0]; lastArg = args[args.length - 1]; } const proxyCall = Object.create(callProto); // ... proxyCall.args = args; proxyCall.firstArg = firstArg; proxyCall.lastArg = lastArg; // ... return proxyCall; }由此可以明确实现事实:
firstArg并非 getter 而是普通实例属性,在 spyCall 创建时一次性写入;- 它直接取自调用时记录的
args[0],因此是对原参数的引用; - 当
args.length === 0时,firstArg保持undefined(局部变量未赋值),不会额外处理; - 同理,
lastArg为args[args.length - 1],两者只在args非空时才有值。
调用链完整回放(可对照 proxy-invoke.js):
- 代理函数被调用,
invoke(func, thisValue, args)执行; - 调用
incrementCallCount并把args、thisValue、callId推入对应数组; - 调用
createCallProperties刷新firstCall/lastCall等快捷属性(内部调用getCall); - 执行原函数,记录返回值/异常;
- 再次调用
createCallProperties,使 spyCall 携带完整的returnValue、exception等。
因此每次访问f.lastCall.firstArg,实际上都会经由getCall(this.callCount - 1)实时构建一个新的 spyCall 对象并读取其firstArg属性。
典型实战场景
场景一:断言函数以特定参数被调用
const sendEmail = sinon.spy(); sendEmail("user@example.com", "欢迎注册", body); sinon.assert.calledWith(sendEmail, "user@example.com"); // 更细粒度:直接检查最后一次调用的首个参数 t.equal(sendEmail.lastCall.firstArg, "user@example.com");场景二:从调用记录中提取回调参数
const fake = sinon.fake(); fake(1, 2, "done"); fake.lastCall.firstArg; // 1 —— 但若首参不是回调,应改用 callback / lastArg注意:firstArg只承诺“第一个参数”,不保证它是函数。若需要提取回调,应使用 callback 属性(它指向最后一个函数参数)或yield/yieldTo系列方法。
场景三:多参数调用中的位置敏感断言
const f = sinon.fake(); f("GET", "/api/users", { limit: 10 }); const call = f.getCall(0); call.firstArg; // "GET" call.args[1]; // "/api/users" call.args[2]; // { limit: 10 }注意事项与边界情况
- 无参数调用返回
undefined:判断“是否携带参数”时请用firstArg !== undefined或检查args.length,不要依赖== null的宽松判断(undefined == null为真,会与显式传null混淆)。 - 显式传入
undefined与不传参数不同:f(undefined)的firstArg是undefined(有参数但值为 undefined),而f()的firstArg也是undefined(无参数)。两者在firstArg上无法区分,需要时用args.length区分。 - 与代理级
firstArg的区别:代理对象上也有firstArg属性(初始为null,见 proxy.js),它记录的是最近一次调用的首参;而spyCall.firstArg是某个特定调用的首参,二者不要混用。多调用场景下请优先使用lastCall.firstArg或getCall(n).firstArg。 - 重置后失效:调用
resetHistory或sinon.reset()后,调用记录清空,lastCall变为null,此时访问null.firstArg会抛错,应先判空(参考 get-call.test.js 中getCall(1) === null的断言)。
相关文档速查
- spy-call 概念总览:spyCall 的定位与
getCall说明 - spyCall API 索引:全部方法与属性列表
- lastArg:与
firstArg对称的末参属性 - args:完整参数数组
- spies 入门:spy 的创建与使用
- fakes 入门:fake 的创建与使用
源码与测试参考:proxy-call.js(spyCall 工厂)、proxy.js(getCall)、proxy-call-util.js(调用属性维护)、first-arg.test.js(官方行为测试)。
- 测试
- 开发工具
【免费下载链接】sinon
Test spies, stubs and mocks for JavaScript.
相关推荐
Sinon 断言系列:`assert.notCalled` 全解析——验证 fake/spy/stub 未被调用的正确姿势
Sinon 断言系列: assert.notCalled 全解析——验证 fake/spy/stub 未被调用的正确姿势 assert.notCalled sp
测试开发工具Sinon `spy.alwaysCalledWithExactly` 深入解析:如何断言每次调用参数都精确一致
Sinon spy.alwaysCalledWithExactly 深入解析:如何断言每次调用参数都精确一致 Sinon 是 JavaScript 生态中最常用
测试开发工具如何用 pascal update 升级 Pascal Editor 本地运行时并在失败时回滚到旧版本
如何用 pascal update 升级 Pascal Editor 本地运行时并在失败时回滚到旧版本 如果你已经在终端里用 Pascal CLI 运行本地 P
测试开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考