1. What breaks first
Shallow mounts that reached into vm.$data, global plugin stubs that do not match Vue 3’s app provide graph, and snapshot tests that are really CSS layout canaries. Replace “snapshot everything” with targeted assertions on behavior.
2. Composables: test the unit of logic
Extract testable pure functions and call composables with @vue/test-utils + explicit plugins and router pinia. Stop testing 400-line SFCs in one mount.
3. Jest or Vitest?
Greenfield on Vite often picks Vitest for config parity. Mature Jest setups can stay on Jest until a quiet quarter—not the same week as the Vue 3 go-live, unless you enjoy two migrations.
4. Mounting script setup components
The single biggest mental shift: in Options API tests, you reached into wrapper.vm.someInternalThing and asserted on it. With <script setup>, those bindings are not exposed on the instance by default. The test surface becomes the rendered DOM, the props/events contract, and any composable you extracted. That is healthier — but only if you adapt the harness.
A typical Vue 3 mount looks like this:
import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import { createRouter, createMemoryHistory } from 'vue-router'
import OrderSummary from '@/components/OrderSummary.vue'
const router = createRouter({
history: createMemoryHistory(),
routes: [{ path: '/', component: { template: '<div />' } }],
})
it('renders the total with currency', async () => {
const wrapper = mount(OrderSummary, {
props: { items: [{ id: 1, price: 4200 }] },
global: {
plugins: [router, createTestingPinia({ stubActions: false })],
stubs: { teleport: true },
},
})
await router.isReady()
expect(wrapper.get('[data-test=total]').text()).toBe('€42.00')
})A few rules worth internalizing: prefer data-test selectors over class names, always await router.isReady() before asserting, and stub teleports unless the test specifically cares about portal placement.
5. Async, Suspense, and flushPromises
Vue 3’s reactivity is async by default. A surprising amount of flaky-test triage in 2026 comes down to missing one await. Three patterns to memorize:
await wrapper.vm.$nextTick()after a state mutation but before a DOM assertion.await flushPromises()after triggering a fetch or any microtask chain.await nextTick(); await flushPromises()when both happen — common with<Suspense>boundaries that resolve async components.
For Suspense components, mount a wrapper component that renders the target inside <Suspense>; do not try to mount an async setup() component directly. The wrapper-and-await pattern reads cleanly and is what the Vue Test Utils docs recommend in 2026.
6. Testing Pinia stores and composables
Pinia’s testing story is genuinely better than Vuex mocks were. Two layers to keep separate:
Store unit tests
Test the store as plain logic — no component required. Use setActivePinia(createPinia()) in beforeEach. This is where business rules live, so coverage here pays back during the Vuex to Pinia migration.
Component-with-store tests
Use createTestingPinia(). Decide explicitly whether to stub actions or not — stubbing is great for asserting “this button dispatched X”, leaving them live is better for end-to-end behavior on a single page.
Composables
Pure composables (no DI) can be tested by calling them inside a tiny mounted component. The pattern of withSetup(() => useThing()) is now common; it gives you the return value without rendering anything real.
7. Jest vs Vitest in 2026: an honest comparison
Both work. The choice depends on the rest of your stack and your appetite for two changes at once.
| Concern | Jest 29+ | Vitest 1+ |
|---|---|---|
| Config parity with Vite | No (separate transform) | Yes (shares vite.config) |
| Watch mode speed | Good | Excellent |
| Mature ecosystem | Largest in JS | Growing fast |
| ESM support | Workable, fiddly | Native |
| Migration cost from Jest | N/A | Low — APIs are nearly identical |
Our default recommendation: if you’re already on Vite, switch to Vitest within a quarter of cutover. If you’re on Webpack with a mature Jest setup, leave it alone until the bundler moves.
8. Anti-patterns we see in real teams
- Snapshot-everything. Snapshots become noise the moment Tailwind classes shift. Use them for stable serialized output (e.g. router meta), not for HTML walls.
- Mounting the world. 800-line specs that mount the root
App.vueand assert on a footer link. Split into focused mounts. - Over-stubbing. Stubbing every child component leaves you testing the test harness, not the code.
- Ignoring real-language tests. If you ship multiple locales, exercise at least one non-English locale per smoke run — see vue-i18n on Vue 3.
- Testing only happy paths. The DOM in Vue 3 is more strict about hydration mismatches; a test for a slow API or 500 response often catches what staging won’t.
9. The pyramid that survives the migration
A migration is a great moment to rebalance the test pyramid, because everyone is paying attention to tests anyway. Our default ratios for a mid-size Vue 3 app:
- ~70% unit and composable tests in Jest/Vitest — fast, run on every commit.
- ~20% component tests with VTU mounts plus targeted Cypress component tests for browser-only behavior (drag/drop, focus traps, intersection observers).
- ~10% end-to-end on critical user journeys, gated to nightly + pre-release.
The exact ratio matters less than having an explicit answer when someone asks “where should this test go?” during a strangler-style rollout.
10. FAQ
Do we have to rewrite every Vue 2 test?
No. Tests that assert on rendered DOM and emitted events port with minor changes (mostly the global option and await sprinkles). Tests that poke at vm.$data or use string-based component names usually do need rewriting.
How do we keep tests green during compat mode?
Run two test commands: one against the Vue 2 build, one against Vue 3. Mark the Vue 3 suite required when its coverage threshold is met. This is part of the migration checklist we use.
Should we use @testing-library/vue instead of VTU?
It is great for behavior-first tests. We see teams pick one as the default and use the other for edge cases. Pick a default and document it, otherwise every PR re-litigates the choice.
Are TypeScript tests worth the effort?
Yes — they catch prop/emit signature drift early. Pair this with incremental TypeScript adoption.
Conclusion
Tests are part of the migration, not a post-launch “tech debt” bucket. Update harnesses, split giant specs, and align the pyramid with release gates. Your future self (and your CI) will say thank you.
