A Vue 3 component you want to test with Vue Testing Library uses a router link, a store, and a value supplied through `provide`/`inject`. How do you get it rendering without reaching into its internals, and what do you fake versus supply for real?
answer
- configure at the boundary, not the instance
- the second argument to render
- a real router beats a hand-written one
- in-memory history for a headless environment
- one shared helper for every test
basics
~20 sSupply the dependencies through render's mounting options — plugins for the router and store, provide for injected values — rather than reaching into the instance. Give real instances where you can, and fake only what crosses the network or cannot run in the test environment.
solid answer
~50 sVue Testing Library's `render(Component, options)` forwards the underlying Vue Test Utils mounting options, so the second argument is where dependencies go: `global.plugins` for a router or store plugin, `global.provide` for injected keys, `global.stubs` for the rare child that cannot run headless, and `props` for ordinary inputs. Prefer real instances over fakes — a real router built with an in-memory history, a real store seeded to the state the scenario needs — because a fake router only proves your fake works, while a real one catches a wrong route name or a guard that redirects. Await the router being ready before asserting on route-dependent output. Fake at genuine boundaries only: the network, and APIs the environment lacks. Then wrap all of it in one `renderWithApp()` helper so every test gets the same environment and no one is tempted to reach for the instance because setup was tedious.
go deeper
Know that dependencies go into the render call's options — plugins for a router or store, provide for injected values — rather than being patched onto the component after mounting.
Explain what each mounting option does and show a working setup for a component behind a router. Be able to say why a real router with in-memory history beats a hand-written object, and why route assertions need to wait for navigation to resolve.
Demonstrate the boundary judgment: real collaborators by default, fakes only where the process boundary genuinely is, and a stated reason for every stub. Show how a shared render helper stops the suite drifting toward instance-poking shortcuts.
Own the test environment as shared infrastructure. Decide what the standard harness installs, keep it in step with the app's global dependencies, and make sure the cheapest path for an engineer writing a new test is also the path that produces a trustworthy one.
## Where dependencies belong The render call takes a second argument, and everything a component needs to exist flows through it. It is forwarded to the Vue Test Utils mount underneath, so the familiar mounting options apply: ```js import { render } from '@testing-library/vue' import { createRouter, createMemoryHistory } from 'vue-router' import OrderSummary from './OrderSummary.vue' import { routes } from '../router/routes' function renderWithApp(component, { route = '/', ...options } = {}) { const router = createRouter({ history: createMemoryHistory(), routes }) router.push(route) return { router, ...render(component, { ...options, global: { plugins: [router], provide: { currency: 'EUR' }, ...options.global, }, }) } } ``` The four options that cover almost everything are `plugins` (anything installed with `app.use` — a router, a store, an i18n instance), `provide` (values a descendant pulls in with `inject`), `stubs` (replace a child component), and `mocks` (replace a global property). Props stay in the top-level `props` option, and in Vue 3 event listeners can be passed there too as `onXxx`. The key discipline: none of this requires touching the component instance. Setup happens at the boundary, and the assertions stay on rendered output. ## Real over fake, wherever it is cheap The instinct to mock every dependency is the biggest quality gap here. A hand-written fake router that returns `{ path: '/orders/7' }` makes the component render, and proves almost nothing: it cannot catch a route whose name was misspelled, a param the component reads under the wrong key, or a navigation guard that would have redirected. A real router constructed with an in-memory history — the history implementation intended for non-browser environments — costs a few lines and exercises the real matching logic. The same holds for a store. Seeding a real store with the state the scenario needs keeps getters, actions and reactivity real, so a test that clicks "Add to cart" and asserts the badge shows 1 proves the wiring end to end. Ecosystem tooling exists for the middle ground — Pinia's testing package can create a store whose actions are stubbed out while state and getters remain real — which is useful when an action would otherwise hit the network. The boundary worth faking is the one your process does not own: HTTP. Faking it at the network layer, rather than by replacing the module your component imports, keeps your own request-building and response-parsing code inside the test. ## The timing trap with routers Router initialisation is asynchronous. A push followed immediately by an assertion on route-dependent markup can run before the navigation resolves, and lazily-loaded route components make it worse. The fix is to await readiness before asserting: ```js router.push('/orders/7') await router.isReady() const { findByRole } = renderWithApp(OrderSummary) expect(await findByRole('heading', { name: /order 7/i })).toBeInTheDocument() ``` The retrying query is the belt to that braces — it tolerates one more resolution step rather than encoding an exact tick count. ## Stub sparingly, and know what it costs Stubbing every child component — the shallow-rendering habit — makes the test fast and nearly meaningless: you end up asserting that placeholder elements exist, and integration bugs between parent and child are exactly what a component test should catch. Worse, a stubbed child renders no accessible markup, so role-based queries stop finding anything and the test drifts toward test ids. Stub for a reason you can state: a child that starts a WebSocket, a mapping widget that needs a canvas the environment does not implement, a component so slow it dominates suite time. Each of those is a boundary; "it was easier" is not. ## Make the right thing the easy thing The practical reason tests reach into internals is that setting up the environment was tedious, so someone shortcut it. A single shared helper — one render function that installs the standard plugins, provides the standard injections, and takes a route and seeded state as arguments — removes that temptation and gives every test the same environment. It also gives you one place to change when the app gains a new global dependency, instead of a hundred test files. ## What an interviewer is listening for They want to hear that you know dependencies are configured at the mount boundary rather than patched onto the instance, that you default to real collaborators and can articulate which boundary genuinely deserves a fake, and that you have a view on the cost of blanket stubbing rather than treating shallow rendering as free speed.
- Someone argues that a fake router object is simpler and faster than constructing a real one. What is your counter?The fake only proves the fake behaves as written. It cannot catch a misspelled route name, a param read under the wrong key, a guard that would have redirected, or a link whose resolved href is wrong. A real router with in-memory history costs a few milliseconds and a helper function, and it turns a class of silent production bugs into failing tests.
- When does stubbing a child component genuinely earn its place?When the child crosses a boundary the test should not own: it opens a socket, needs an API the environment does not implement, or dominates runtime. State the reason in the test. Blanket stubbing is different — it reduces the test to asserting that placeholders rendered, hides parent-child integration bugs, and strips the accessible markup your role queries depend on.
- Your component injects a value that a distant ancestor provides. How do you supply it without rendering the whole ancestor chain?Pass it through the mounting options' provide map, keyed exactly as the component injects it — the same string or symbol key, imported from the module that defines it rather than retyped. That keeps the test focused on the component under test. If the key is a symbol you had to guess at, that is a signal the provide contract deserves a shared exported constant.
saying these in an interview costs you the question
- Mocks the router and store by default without asking why
- Asserts on route markup before navigation has resolved
- Shallow-renders everything and calls the result an integration test
- Reaches into the instance because setup felt awkward
- Retypes an inject key by hand instead of importing it