Skip to content

Commit 3751155

Browse files
authored
Add unit-test-vue-pinia skill (#1005)
1 parent 20afb96 commit 3751155

3 files changed

Lines changed: 294 additions & 0 deletions

File tree

docs/README.skills.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-skills) for guidelines on how to
234234
| [typespec-api-operations](../skills/typespec-api-operations/SKILL.md) | Add GET, POST, PATCH, and DELETE operations to a TypeSpec API plugin with proper routing, parameters, and adaptive cards | None |
235235
| [typespec-create-agent](../skills/typespec-create-agent/SKILL.md) | Generate a complete TypeSpec declarative agent with instructions, capabilities, and conversation starters for Microsoft 365 Copilot | None |
236236
| [typespec-create-api-plugin](../skills/typespec-create-api-plugin/SKILL.md) | Generate a TypeSpec API plugin with REST operations, authentication, and Adaptive Cards for Microsoft 365 Copilot | None |
237+
| [unit-test-vue-pinia](../skills/unit-test-vue-pinia/SKILL.md) | Write and review unit tests for Vue 3 + TypeScript + Vitest + Pinia codebases. Use when creating or updating tests for components, composables, and stores; mocking Pinia with createTestingPinia; applying Vue Test Utils patterns; and enforcing black-box assertions over implementation details. | `references/pinia-patterns.md` |
237238
| [update-avm-modules-in-bicep](../skills/update-avm-modules-in-bicep/SKILL.md) | Update Azure Verified Modules (AVM) to latest versions in Bicep files. | None |
238239
| [update-implementation-plan](../skills/update-implementation-plan/SKILL.md) | Update an existing implementation plan file with new or update requirements to provide new features, refactoring existing code or upgrading packages, design, architecture or infrastructure. | None |
239240
| [update-llms](../skills/update-llms/SKILL.md) | Update the llms.txt file in the root folder to reflect changes in documentation or specifications following the llms.txt specification at https://llmstxt.org/ | None |
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
---
2+
name: unit-test-vue-pinia
3+
category: testing
4+
description: 'Write and review unit tests for Vue 3 + TypeScript + Vitest + Pinia codebases. Use when creating or updating tests for components, composables, and stores; mocking Pinia with createTestingPinia; applying Vue Test Utils patterns; and enforcing black-box assertions over implementation details.'
5+
---
6+
7+
# unit-test-vue-pinia
8+
9+
Use this skill to create or review unit tests for Vue components, composables, and Pinia stores. Keep tests small, deterministic, and behavior-first.
10+
11+
## Workflow
12+
13+
1. Identify the behavior boundary first: component UI behavior, composable behavior, or store behavior.
14+
2. Choose the narrowest test style that can prove that behavior.
15+
3. Set up Pinia with the least powerful option that still covers the scenario.
16+
4. Drive the test through public inputs such as props, form updates, button clicks, emitted child events, and store APIs.
17+
5. Assert observable outputs and side effects before considering any instance-level assertion.
18+
6. Return or review tests with clear behavior-oriented names and note any remaining coverage gaps.
19+
20+
## Core Rules
21+
22+
- Test one behavior per test.
23+
- Assert observable input/output behavior first (rendered text, emitted events, callback calls, store state changes).
24+
- Avoid implementation-coupled assertions.
25+
- Access `wrapper.vm` only in exceptional cases when there is no reasonable DOM, prop, emit, or store-level assertion.
26+
- Prefer explicit setup in `beforeEach()` and reset mocks every test.
27+
- Use checked-in reference material in `references/pinia-patterns.md` as the local source of truth for standard Pinia test setups.
28+
29+
## Pinia Testing Approach
30+
31+
Use `references/pinia-patterns.md` first, then fall back to Pinia's testing cookbook when the checked-in examples do not cover the case.
32+
33+
### Default pattern for component tests
34+
35+
Use `createTestingPinia` as a global plugin while mounting.
36+
Prefer `createSpy: vi.fn` as the default for consistency and easier action-spy assertions.
37+
38+
```ts
39+
const wrapper = mount(ComponentUnderTest, {
40+
global: {
41+
plugins: [
42+
createTestingPinia({
43+
createSpy: vi.fn,
44+
}),
45+
],
46+
},
47+
});
48+
```
49+
50+
By default, actions are stubbed and spied.
51+
Use `stubActions: true` (default) when the test only needs to verify whether an action was called (or not called).
52+
53+
### Accepted minimal Pinia setups
54+
55+
The following are also valid and should not be flagged as incorrect:
56+
57+
- `createTestingPinia({})` when the test does not assert Pinia action spy behavior.
58+
- `createTestingPinia({ initialState: ... })` or `createTestingPinia({ stubActions: ... })` without `createSpy`, when the test only needs state seeding or action stubbing behavior and does not inspect generated spies.
59+
- `setActivePinia(createTestingPinia(...))` in store/composable-focused tests (without mounting a component) when mocking/seeding dependent stores is needed.
60+
61+
Use `createSpy: vi.fn` when action spy assertions are part of the test intent.
62+
63+
### Execute real actions only when needed
64+
65+
Use `stubActions: false` only when the test must validate the action's real behavior and side effects. Do not switch it on by default for simple "was called" assertions.
66+
67+
```ts
68+
const wrapper = mount(ComponentUnderTest, {
69+
global: {
70+
plugins: [
71+
createTestingPinia({
72+
createSpy: vi.fn,
73+
stubActions: false,
74+
}),
75+
],
76+
},
77+
});
78+
```
79+
80+
### Seed store state with `initialState`
81+
82+
```ts
83+
const wrapper = mount(ComponentUnderTest, {
84+
global: {
85+
plugins: [
86+
createTestingPinia({
87+
createSpy: vi.fn,
88+
initialState: {
89+
counter: { n: 20 },
90+
user: { name: "Leia Organa" },
91+
},
92+
}),
93+
],
94+
},
95+
});
96+
```
97+
98+
### Add Pinia plugins through `createTestingPinia`
99+
100+
```ts
101+
const wrapper = mount(ComponentUnderTest, {
102+
global: {
103+
plugins: [
104+
createTestingPinia({
105+
createSpy: vi.fn,
106+
plugins: [myPiniaPlugin],
107+
}),
108+
],
109+
},
110+
});
111+
```
112+
113+
### Getter override pattern for edge cases
114+
115+
```ts
116+
const pinia = createTestingPinia({ createSpy: vi.fn });
117+
const store = useCounterStore(pinia);
118+
119+
store.double = 999;
120+
// @ts-expect-error test-only reset of overridden getter
121+
store.double = undefined;
122+
```
123+
124+
### Pure store unit tests
125+
126+
Prefer pure store tests with `createPinia()` when the goal is to validate store state transitions and action behavior without component rendering. Use `createTestingPinia()` only when you need stubbed dependent stores, seeded test doubles, or action spies.
127+
128+
```ts
129+
beforeEach(() => {
130+
setActivePinia(createPinia());
131+
});
132+
133+
it("increments", () => {
134+
const counter = useCounterStore();
135+
counter.increment();
136+
expect(counter.n).toBe(1);
137+
});
138+
```
139+
140+
## Vue Test Utils Approach
141+
142+
Follow Vue Test Utils guidance: <https://test-utils.vuejs.org/guide/>
143+
144+
- Mount shallow by default for focused unit tests.
145+
- Mount full component trees only when integration behavior is the subject.
146+
- Drive behavior through props, user-like interactions, and emitted events.
147+
- Prefer `findComponent(...).vm.$emit(...)` for child stub events instead of touching parent internals.
148+
- Use `nextTick` only when updates are async.
149+
- Assert emitted events and payloads with `wrapper.emitted(...)`.
150+
- Access `wrapper.vm` only when no DOM assertion, emitted event assertion, prop assertion, or store-level assertion can express the behavior. Treat it as an exception and keep the assertion narrowly scoped.
151+
152+
## Key Testing Snippets
153+
154+
Emit and assert payload:
155+
156+
```ts
157+
await wrapper.find("button").trigger("click");
158+
expect(wrapper.emitted("submit")?.[0]?.[0]).toBe("Mango Mission");
159+
```
160+
161+
Update input and assert output:
162+
163+
```ts
164+
await wrapper.find("input").setValue("Agent Violet");
165+
await wrapper.find("form").trigger("submit");
166+
expect(wrapper.emitted("save")?.[0]?.[0]).toBe("Agent Violet");
167+
```
168+
169+
## Test Writing Workflow
170+
171+
1. Identify the behavior boundary to test.
172+
2. Build minimal fixture data (only fields needed by that behavior).
173+
3. Configure Pinia and required test doubles.
174+
4. Trigger behavior through public inputs.
175+
5. Assert public outputs and side effects.
176+
6. Refactor test names to describe behavior, not implementation.
177+
178+
## Constraints and Safety
179+
180+
- Do not test private/internal implementation details.
181+
- Do not overuse snapshots for dynamic UI behavior.
182+
- Do not assert every field in large objects if only one behavior matters.
183+
- Keep fake data deterministic; avoid random values.
184+
- Do not claim a Pinia setup is wrong when it is one of the accepted minimal setups above.
185+
- Do not rewrite working tests toward deeper mounting or real actions unless the behavior under test requires that extra surface area.
186+
- Flag missing test coverage, brittle selectors, and implementation-coupled assertions explicitly during review.
187+
188+
## Output Contract
189+
190+
- For `create` or `update`, return the finished test code plus a short note describing the selected Pinia strategy.
191+
- For `review`, return concrete findings first, then missing coverage or brittleness risks.
192+
- When the safest choice is ambiguous, state the assumption that drove the chosen test setup.
193+
194+
## References
195+
196+
- `references/pinia-patterns.md`
197+
- Pinia testing cookbook: <https://pinia.vuejs.org/cookbook/testing.html>
198+
- Vue Test Utils guide: <https://test-utils.vuejs.org/guide/>
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Pinia Testing Snippets (Cookbook-Aligned)
2+
3+
Use these patterns directly when writing tests with `@pinia/testing`.
4+
5+
## Component mount with `createTestingPinia`
6+
7+
```ts
8+
import { mount } from "@vue/test-utils";
9+
import { createTestingPinia } from "@pinia/testing";
10+
import { vi } from "vitest";
11+
12+
const wrapper = mount(ComponentUnderTest, {
13+
global: {
14+
plugins: [
15+
createTestingPinia({
16+
createSpy: vi.fn,
17+
}),
18+
],
19+
},
20+
});
21+
```
22+
23+
## Execute real actions
24+
25+
Use this only when behavior inside the action must run.
26+
If the test only checks call/no-call expectations, keep default stubbing (`stubActions: true`).
27+
28+
```ts
29+
const wrapper = mount(ComponentUnderTest, {
30+
global: {
31+
plugins: [
32+
createTestingPinia({
33+
createSpy: vi.fn,
34+
stubActions: false,
35+
}),
36+
],
37+
},
38+
});
39+
```
40+
41+
## Seed starting state
42+
43+
```ts
44+
const wrapper = mount(ComponentUnderTest, {
45+
global: {
46+
plugins: [
47+
createTestingPinia({
48+
createSpy: vi.fn,
49+
initialState: {
50+
counter: { n: 10 },
51+
profile: { name: "Sherlock Holmes" },
52+
},
53+
}),
54+
],
55+
},
56+
});
57+
```
58+
59+
## Use store in test and assert action call
60+
61+
```ts
62+
const pinia = createTestingPinia({ createSpy: vi.fn });
63+
const store = useCounterStore(pinia);
64+
65+
store.increment();
66+
expect(store.increment).toHaveBeenCalledTimes(1);
67+
```
68+
69+
## Add plugin under test
70+
71+
```ts
72+
const wrapper = mount(ComponentUnderTest, {
73+
global: {
74+
plugins: [
75+
createTestingPinia({
76+
createSpy: vi.fn,
77+
plugins: [myPiniaPlugin],
78+
}),
79+
],
80+
},
81+
});
82+
```
83+
84+
## Override and reset getters for edge tests
85+
86+
```ts
87+
const pinia = createTestingPinia({ createSpy: vi.fn });
88+
const store = useCounterStore(pinia);
89+
90+
store.double = 42;
91+
expect(store.double).toBe(42);
92+
93+
// @ts-expect-error test-only reset
94+
store.double = undefined;
95+
```

0 commit comments

Comments
 (0)