Skip to content

Commit cff409e

Browse files
doubledare704posva
andauthored
feat: add selective action stubbing support (#3040)
Co-authored-by: Eduardo San Martin Morote <[email protected]>
1 parent 126e031 commit cff409e

File tree

4 files changed

+353
-10
lines changed

4 files changed

+353
-10
lines changed

packages/docs/cookbook/testing.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,80 @@ store.someAction()
166166
expect(store.someAction).toHaveBeenCalledTimes(1)
167167
```
168168

169+
### Selective action stubbing
170+
171+
Sometimes you may want to stub only specific actions while allowing others to execute normally. You can achieve this by passing an array of action names to the `stubActions` option:
172+
173+
```js
174+
// Only stub the 'increment' and 'reset' actions
175+
const wrapper = mount(Counter, {
176+
global: {
177+
plugins: [
178+
createTestingPinia({
179+
stubActions: ['increment', 'reset'],
180+
}),
181+
],
182+
},
183+
})
184+
185+
const store = useSomeStore()
186+
187+
// These actions will be stubbed (not executed)
188+
store.increment() // stubbed
189+
store.reset() // stubbed
190+
191+
// Other actions will execute normally but still be spied
192+
store.fetchData() // executed normally
193+
expect(store.fetchData).toHaveBeenCalledTimes(1)
194+
```
195+
196+
For more complex scenarios, you can pass a function that receives the action name and store instance, and returns whether the action should be stubbed:
197+
198+
```js
199+
// Stub actions based on custom logic
200+
const wrapper = mount(Counter, {
201+
global: {
202+
plugins: [
203+
createTestingPinia({
204+
stubActions: (actionName, store) => {
205+
// Stub all actions that start with 'set'
206+
if (actionName.startsWith('set')) return true
207+
208+
// Stub actions based on initial store state
209+
if (store.isPremium) return false
210+
211+
return true
212+
},
213+
}),
214+
],
215+
},
216+
})
217+
218+
const store = useSomeStore()
219+
220+
// Actions starting with 'set' are stubbed
221+
store.setValue(42) // stubbed
222+
223+
// Other actions may execute based on the initial store state
224+
store.fetchData() // executed or stubbed based on initial store.isPremium
225+
```
226+
227+
::: tip
228+
229+
- An empty array `[]` means no actions will be stubbed (same as `false`)
230+
- The function is evaluated once at store setup time, receiving the store instance in its initial state
231+
232+
:::
233+
234+
You can also manually mock specific actions after creating the store:
235+
236+
```ts
237+
const store = useSomeStore()
238+
vi.spyOn(store, 'increment').mockImplementation(() => {})
239+
// or if using testing pinia with stubbed actions
240+
store.increment.mockImplementation(() => {})
241+
```
242+
169243
### Mocking the returned value of an action
170244

171245
Actions are automatically spied but type-wise, they are still the regular actions. In order to get the correct type, we must implement a custom type-wrapper that applies the `Mock` type to each action. **This type depends on the testing framework you are using**. Here is an example with Vitest:

packages/docs/zh/cookbook/testing.md

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,79 @@ store.someAction()
173173
expect(store.someAction).toHaveBeenCalledTimes(1)
174174
```
175175

176-
<!-- TODO: translation -->
176+
### 选择性 action 存根 %{#selective-action-stubbing}%
177+
178+
有时你可能只想存根特定的 action,而让其他 action 正常执行。你可以通过向 `stubActions` 选项传递一个 action 名称数组来实现:
179+
180+
```js
181+
// 只存根 'increment' 和 'reset' action
182+
const wrapper = mount(Counter, {
183+
global: {
184+
plugins: [
185+
createTestingPinia({
186+
stubActions: ['increment', 'reset'],
187+
}),
188+
],
189+
},
190+
})
191+
192+
const store = useSomeStore()
193+
194+
// 这些 action 将被存根(不执行)
195+
store.increment() // 存根
196+
store.reset() // 存根
197+
198+
// 其他 action 将正常执行但仍被监听
199+
store.fetchData() // 正常执行
200+
expect(store.fetchData).toHaveBeenCalledTimes(1)
201+
```
202+
203+
对于更复杂的场景,你可以传递一个函数,该函数接收 action 名称和 store 实例,并返回是否应该存根该 action:
204+
205+
```js
206+
// 基于自定义逻辑存根 action
207+
const wrapper = mount(Counter, {
208+
global: {
209+
plugins: [
210+
createTestingPinia({
211+
stubActions: (actionName, store) => {
212+
// 存根所有以 'set' 开头的 action
213+
if (actionName.startsWith('set')) return true
214+
215+
// 根据初始 store 状态存根 action
216+
if (store.isPremium) return false
217+
218+
return true
219+
},
220+
}),
221+
],
222+
},
223+
})
224+
225+
const store = useSomeStore()
226+
227+
// 以 'set' 开头的 action 被存根
228+
store.setValue(42) // 存根
229+
230+
// 其他 action 可能根据初始 store 状态执行
231+
store.fetchData() // 根据初始 store.isPremium 执行或存根
232+
```
233+
234+
::: tip
235+
236+
- 空数组 `[]` 表示不存根任何 action(与 `false` 相同)
237+
- 函数在 store 设置时被评估一次,接收处于初始状态的 store 实例
238+
239+
:::
240+
241+
你也可以在创建 store 后手动模拟特定的 action:
242+
243+
```ts
244+
const store = useSomeStore()
245+
vi.spyOn(store, 'increment').mockImplementation(() => {})
246+
// 或者如果使用带有存根 action 的测试 pinia
247+
store.increment.mockImplementation(() => {})
248+
```
177249

178250
### Mocking the returned value of an action
179251

packages/testing/src/testing.spec.ts

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ describe('Testing', () => {
2121
increment(amount = 1) {
2222
this.n += amount
2323
},
24+
decrement() {
25+
this.n--
26+
},
27+
setValue(newValue: number) {
28+
this.n = newValue
29+
},
2430
},
2531
})
2632

@@ -35,6 +41,12 @@ describe('Testing', () => {
3541
function increment(amount = 1) {
3642
n.value += amount
3743
}
44+
function decrement() {
45+
n.value--
46+
}
47+
function setValue(newValue: number) {
48+
n.value = newValue
49+
}
3850
function $reset() {
3951
n.value = 0
4052
}
@@ -45,6 +57,8 @@ describe('Testing', () => {
4557
double,
4658
doublePlusOne,
4759
increment,
60+
decrement,
61+
setValue,
4862
$reset,
4963
}
5064
})
@@ -326,6 +340,154 @@ describe('Testing', () => {
326340
storeToRefs(store)
327341
expect(store.doubleComputedCallCount).toBe(0)
328342
})
343+
344+
describe('selective action stubbing', () => {
345+
it('stubs only actions in array', () => {
346+
setActivePinia(
347+
createTestingPinia({
348+
stubActions: ['increment', 'setValue'],
349+
createSpy: vi.fn,
350+
})
351+
)
352+
353+
const store = useStore()
354+
355+
// Actions in array should be stubbed (not execute)
356+
store.increment()
357+
expect(store.n).toBe(0) // Should not change
358+
expect(store.increment).toHaveBeenCalledTimes(1)
359+
360+
store.setValue(42)
361+
expect(store.n).toBe(0) // Should not change
362+
expect(store.setValue).toHaveBeenCalledTimes(1)
363+
expect(store.setValue).toHaveBeenLastCalledWith(42)
364+
365+
// Actions not in array should execute normally but still be spied
366+
store.decrement()
367+
expect(store.n).toBe(-1) // Should change
368+
expect(store.decrement).toHaveBeenCalledTimes(1)
369+
})
370+
371+
it('handles empty array (same as false)', () => {
372+
setActivePinia(
373+
createTestingPinia({
374+
stubActions: [],
375+
createSpy: vi.fn,
376+
})
377+
)
378+
379+
const store = useStore()
380+
381+
// All actions should execute normally
382+
store.increment()
383+
expect(store.n).toBe(1) // Should change
384+
expect(store.increment).toHaveBeenCalledTimes(1)
385+
386+
store.setValue(42)
387+
expect(store.n).toBe(42) // Should change
388+
expect(store.setValue).toHaveBeenCalledTimes(1)
389+
})
390+
391+
it('handles non-existent action names gracefully', () => {
392+
setActivePinia(
393+
createTestingPinia({
394+
stubActions: ['increment', 'nonExistentAction'],
395+
createSpy: vi.fn,
396+
})
397+
)
398+
399+
const store = useStore()
400+
401+
// Should work normally despite non-existent action in array
402+
store.increment()
403+
expect(store.n).toBe(0) // Should not change
404+
expect(store.increment).toHaveBeenCalledTimes(1)
405+
406+
store.setValue(42)
407+
expect(store.n).toBe(42) // Should change (not in array)
408+
expect(store.setValue).toHaveBeenCalledTimes(1)
409+
})
410+
411+
it('stubs actions based on function predicate', () => {
412+
setActivePinia(
413+
createTestingPinia({
414+
stubActions: (actionName) =>
415+
actionName.startsWith('set') || actionName === 'decrement',
416+
createSpy: vi.fn,
417+
})
418+
)
419+
420+
const store = useStore()
421+
422+
// setValue should be stubbed (starts with 'set')
423+
store.setValue(42)
424+
expect(store.n).toBe(0) // Should not change
425+
expect(store.setValue).toHaveBeenCalledTimes(1)
426+
427+
// increment should execute (doesn't match predicate)
428+
store.increment()
429+
expect(store.n).toBe(1) // Should change
430+
expect(store.increment).toHaveBeenCalledTimes(1)
431+
432+
// decrement should be stubbed (matches predicate)
433+
store.decrement()
434+
expect(store.n).toBe(1) // Should not change (stubbed)
435+
expect(store.decrement).toHaveBeenCalledTimes(1)
436+
})
437+
438+
it('function predicate receives correct store instance', () => {
439+
const predicateSpy = vi.fn(() => false)
440+
441+
setActivePinia(
442+
createTestingPinia({
443+
stubActions: predicateSpy,
444+
createSpy: vi.fn,
445+
})
446+
)
447+
448+
const store = useStore()
449+
450+
expect(predicateSpy).toHaveBeenCalledWith('increment', store)
451+
})
452+
453+
it('can stub all actions (default)', () => {
454+
setActivePinia(
455+
createTestingPinia({
456+
stubActions: true,
457+
createSpy: vi.fn,
458+
})
459+
)
460+
461+
const store = useStore()
462+
463+
store.increment()
464+
expect(store.n).toBe(0) // Should not change
465+
expect(store.increment).toHaveBeenCalledTimes(1)
466+
467+
store.setValue(42)
468+
expect(store.n).toBe(0) // Should not change
469+
expect(store.setValue).toHaveBeenCalledTimes(1)
470+
})
471+
472+
it('can not stub any action', () => {
473+
setActivePinia(
474+
createTestingPinia({
475+
stubActions: false,
476+
createSpy: vi.fn,
477+
})
478+
)
479+
480+
const store = useStore()
481+
482+
store.increment()
483+
expect(store.n).toBe(1) // Should change
484+
expect(store.increment).toHaveBeenCalledTimes(1)
485+
486+
store.setValue(42)
487+
expect(store.n).toBe(42) // Should change
488+
expect(store.setValue).toHaveBeenCalledTimes(1)
489+
})
490+
})
329491
}
330492

331493
it('works with no actions', () => {

0 commit comments

Comments
 (0)