Skip to content

Commit 67c8e13

Browse files
authored
feat(core): support using for mock restore (#1293)
1 parent fd500f7 commit 67c8e13

7 files changed

Lines changed: 138 additions & 2 deletions

File tree

e2e/spy/dispose.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { describe, expect, it, rstest } from '@rstest/core';
2+
3+
describe('mock dispose', () => {
4+
it('restores spies when leaving a using scope', () => {
5+
const hi = {
6+
sayHi: () => 'hi',
7+
};
8+
9+
{
10+
using spy = rstest.spyOn(hi, 'sayHi').mockImplementation(() => 'hello');
11+
12+
expect(hi.sayHi()).toBe('hello');
13+
expect(spy).toHaveBeenCalledTimes(1);
14+
}
15+
16+
expect(hi.sayHi()).toBe('hi');
17+
expect(rstest.isMockFunction(hi.sayHi)).toBeFalsy();
18+
});
19+
20+
it('resets mock functions when disposed manually', () => {
21+
const sayHi = rstest.fn(() => 'hi').mockImplementation(() => 'hello');
22+
23+
expect(sayHi()).toBe('hello');
24+
25+
sayHi[Symbol.dispose]();
26+
27+
expect(sayHi()).toBe('hi');
28+
});
29+
});

packages/core/src/runtime/api/spy.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,15 @@ export const initSpy = (): Pick<
204204
mockName = mockFn?.name;
205205
};
206206

207+
if (Symbol.dispose) {
208+
Object.defineProperty(spyFn, Symbol.dispose, {
209+
value: () => {
210+
spyFn.mockRestore();
211+
},
212+
configurable: true,
213+
});
214+
}
215+
207216
mocks.add(spyFn);
208217

209218
return spyFn;

packages/core/src/types/mock.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ export interface MockInstance<T extends FunctionLike = FunctionLike> {
116116
* Does what `mockReset` does and restores original descriptors of spied-on objects.
117117
*/
118118
mockRestore(): void;
119+
/**
120+
* Restores the mock when it leaves a `using` scope.
121+
*/
122+
[Symbol.dispose](): void;
119123
/**
120124
* Returns current mock implementation if there is one.
121125
*/

website/docs/en/api/runtime-api/rstest/mock-instance.mdx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ description: MockInstance is the type of all mock/spy instances, providing a ric
44
overviewHeaders: [2, 3]
55
---
66

7+
import { ApiMeta } from '@components/ApiMeta';
8+
79
# Mock instance
810

911
`MockInstance` is the type of all mock/spy instances, providing a rich set of APIs for controlling and inspecting mocks.
@@ -58,7 +60,7 @@ fn.mockReset();
5860

5961
## mockRestore
6062

61-
- **Type:** `() => MockInstance`
63+
- **Type:** `() => void`
6264

6365
Restores the original method of a spied object (only effective for spies).
6466

@@ -68,6 +70,25 @@ const spy = rstest.spyOn(obj, 'foo');
6870
spy.mockRestore();
6971
```
7072

73+
## Symbol.dispose
74+
75+
<ApiMeta addedVersion="0.10.2" />
76+
77+
- **Type:** `() => void`
78+
79+
Calls `mockRestore()` when the mock leaves a `using` scope.
80+
81+
```ts
82+
const obj = { foo: () => 1 };
83+
84+
{
85+
using spy = rstest.spyOn(obj, 'foo').mockImplementation(() => 2);
86+
expect(obj.foo()).toBe(2);
87+
}
88+
89+
expect(obj.foo()).toBe(1);
90+
```
91+
7192
## getMockImplementation
7293

7394
- **Type:** `() => Function | undefined`

website/docs/en/guide/basic/mock.mdx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
description: Mock functions, object methods, ESM modules, and CommonJS modules in Rstest, and distinguish mock state from module state.
33
---
44

5+
import { ApiMeta } from '@components/ApiMeta';
6+
57
# Mocking
68

79
Mocking lets you replace dependencies in tests, control return values, and assert how functions or modules are called. Rstest provides different mocking APIs for functions, object methods, ESM modules, CommonJS modules, and object trees.
@@ -211,6 +213,30 @@ test('logs a warning when validation fails', () => {
211213
});
212214
```
213215

216+
### `using` syntax
217+
218+
<ApiMeta addedVersion="0.10.2" />
219+
220+
Rstest supports the `using` syntax to restore the spy automatically when the block exits:
221+
222+
```ts title="logger.test.ts"
223+
import { expect, rstest, test } from '@rstest/core';
224+
225+
test('logs a warning when validation fails', () => {
226+
{
227+
using warn = rstest
228+
.spyOn(console, 'warn')
229+
.mockImplementation(() => undefined);
230+
231+
console.warn('invalid payload');
232+
233+
expect(warn).toHaveBeenCalledWith('invalid payload');
234+
}
235+
236+
// console.warn is restored here
237+
});
238+
```
239+
214240
This pattern is commonly used with globals such as `console` and `Date`, as well as shared objects that already exist in the test.
215241

216242
## Deep-mock objects

website/docs/zh/api/runtime-api/rstest/mock-instance.mdx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ description: MockInstance 是所有 mock 和 spy 实例的类型。
44
overviewHeaders: [2, 3]
55
---
66

7+
import { ApiMeta } from '@components/ApiMeta';
8+
79
# Mock instance
810

911
`MockInstance` 是所有 mock 和 spy 实例的类型。
@@ -58,7 +60,7 @@ fn.mockReset();
5860

5961
## mockRestore
6062

61-
- **类型:** `() => MockInstance`
63+
- **类型:** `() => void`
6264

6365
恢复被 spy 的对象的原始方法(仅对 spy 有效)。
6466

@@ -68,6 +70,25 @@ const spy = rstest.spyOn(obj, 'foo');
6870
spy.mockRestore();
6971
```
7072

73+
## Symbol.dispose
74+
75+
<ApiMeta addedVersion="0.10.2" />
76+
77+
- **类型:** `() => void`
78+
79+
当 mock 离开 `using` 作用域时调用 `mockRestore()`
80+
81+
```ts
82+
const obj = { foo: () => 1 };
83+
84+
{
85+
using spy = rstest.spyOn(obj, 'foo').mockImplementation(() => 2);
86+
expect(obj.foo()).toBe(2);
87+
}
88+
89+
expect(obj.foo()).toBe(1);
90+
```
91+
7192
## getMockImplementation
7293

7394
- **类型:** `() => Function | undefined`

website/docs/zh/guide/basic/mock.mdx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
description: 使用 Rstest mock 函数、对象方法、ESM 模块、CommonJS 模块,并区分 mock 状态和模块状态。
33
---
44

5+
import { ApiMeta } from '@components/ApiMeta';
6+
57
# Mocking
68

79
Mocking 用于在测试中替换依赖、控制返回值,并断言函数或模块的调用行为。Rstest 提供了多组 mock API,分别适用于函数、对象方法、ESM 模块、CommonJS 模块和对象树。
@@ -211,6 +213,30 @@ test('logs a warning when validation fails', () => {
211213
});
212214
```
213215

216+
### `using` 语法
217+
218+
<ApiMeta addedVersion="0.10.2" />
219+
220+
Rstest 支持使用 `using` 语法在代码块退出时自动恢复 spy:
221+
222+
```ts title="logger.test.ts"
223+
import { expect, rstest, test } from '@rstest/core';
224+
225+
test('logs a warning when validation fails', () => {
226+
{
227+
using warn = rstest
228+
.spyOn(console, 'warn')
229+
.mockImplementation(() => undefined);
230+
231+
console.warn('invalid payload');
232+
233+
expect(warn).toHaveBeenCalledWith('invalid payload');
234+
}
235+
236+
// console.warn 在这里已恢复
237+
});
238+
```
239+
214240
这类写法常用于 `console``Date` 这类全局对象,以及测试里已经存在的共享对象。
215241

216242
## 深度 mock 对象

0 commit comments

Comments
 (0)