diff --git a/e2e/spy/spyOn.test.ts b/e2e/spy/spyOn.test.ts index 1378d7c28..cf4fdd634 100644 --- a/e2e/spy/spyOn.test.ts +++ b/e2e/spy/spyOn.test.ts @@ -78,4 +78,15 @@ describe('test spyOn', () => { spy.mockReset(); expect(util1.sayHi()).toBe('hi'); }); + + it('spyOn re-spy', () => { + const hi = { + sayHi: () => 'hi', + }; + rstest.spyOn(hi, 'sayHi').mockImplementation(() => 'hello'); + + expect(hi.sayHi()).toBe('hello'); + // should get the same spy instance + expect(rstest.spyOn(hi, 'sayHi')).toBeCalled(); + }); }); diff --git a/packages/core/src/runtime/api/spy.ts b/packages/core/src/runtime/api/spy.ts index adf15012d..a688fd5d5 100644 --- a/packages/core/src/runtime/api/spy.ts +++ b/packages/core/src/runtime/api/spy.ts @@ -226,6 +226,22 @@ export const initSpy = (): Pick< methodName: K, accessType?: 'get' | 'set', ): MockInstance => { + if (accessType) { + const descriptor = Object.getOwnPropertyDescriptor(obj, methodName); + const accessor = + accessType === 'get' + ? Reflect.get(descriptor ?? {}, 'get') + : Reflect.get(descriptor ?? {}, 'set'); + if (typeof accessor === 'function' && isMockFunction(accessor)) { + return accessor as MockInstance; + } + } else { + const method = obj[methodName]; + if (isMockFunction(method)) { + return method; + } + } + const accessTypeMap = { get: 'getter', set: 'setter', diff --git a/website/docs/en/api/runtime-api/rstest/mock-functions.mdx b/website/docs/en/api/runtime-api/rstest/mock-functions.mdx index 06108b4c8..5e8d7d570 100644 --- a/website/docs/en/api/runtime-api/rstest/mock-functions.mdx +++ b/website/docs/en/api/runtime-api/rstest/mock-functions.mdx @@ -59,6 +59,19 @@ expect(hi.sayHi()).toBe('hi'); expect(spy).toHaveBeenCalled(); ``` +If you call `rstest.spyOn` multiple times for the same method, Rstest reuses the existing spy instead of redefining it. + +```ts +const hi = { + sayHi: () => 'hi', +}; +rstest.spyOn(hi, 'sayHi').mockImplementation(() => 'hello'); + +expect(hi.sayHi()).toBe('hello'); +// should get the same spy instance +expect(rstest.spyOn(hi, 'sayHi')).toBeCalled(); +``` + ## rstest.isMockFunction - **Alias:** `rs.isMockFunction` diff --git a/website/docs/zh/api/runtime-api/rstest/mock-functions.mdx b/website/docs/zh/api/runtime-api/rstest/mock-functions.mdx index 7ed456972..fc1573ffd 100644 --- a/website/docs/zh/api/runtime-api/rstest/mock-functions.mdx +++ b/website/docs/zh/api/runtime-api/rstest/mock-functions.mdx @@ -59,6 +59,20 @@ expect(hi.sayHi()).toBe('hi'); expect(spy).toHaveBeenCalled(); ``` +对同一个方法重复调用 `rstest.spyOn` 时,会返回已有的 spy,而不是重新定义。 + +```ts +const hi = { + sayHi: () => 'hi', +}; +rstest.spyOn(hi, 'sayHi').mockImplementation(() => 'hello'); + +expect(hi.sayHi()).toBe('hello'); + +// 返回的 spy 实例与第一次调用相同 +expect(rstest.spyOn(hi, 'sayHi')).toBeCalled(); +``` + ## rstest.isMockFunction - **别名:** `rs.isMockFunction`