With Vue Test Utils, how do you stub only a dashboard's heavy <SalesChart> child and still verify the data the dashboard hands it?
answer
- one entry, not shallow for everything
- the stub remembers the original
- props survive on the stub
- find vs findComponent return types
- emit from the stand-in
basics
~10 sMount the dashboard normally with global.stubs: { SalesChart: true }. The chart becomes an empty <sales-chart-stub> that keeps SalesChart's props, so wrapper.findComponent(SalesChart).props('series') returns exactly what the dashboard passed.
solid answer
~50 sUse a full `mount` and stub only the expensive child: `global: { stubs: { SalesChart: true } }`. The chart's setup, canvas work and hooks never run; it renders as `<sales-chart-stub>`. The automatic stub copies `SalesChart`'s props declaration and Vue Test Utils maps the stub back to the original, so `wrapper.findComponent(SalesChart)` returns a `VueWrapper` and `.props('series')` gives the value the dashboard computed — assert on that, not on the stub's stringified attributes. `find('sales-chart-stub')` would only return a DOM wrapper with no `props()`. To test how the dashboard reacts to the chart, call `findComponent(SalesChart).vm.$emit('point-select', point)` and assert on the dashboard's DOM, remembering the stub will accept any event name. A hand-written stub object (`{ template: '<div />' }`) is fine too, but declare the props on it — otherwise the bindings become attrs and `props()` is empty.
code
ts · 18 linesimport { mount } from '@vue/test-utils'
import { nextTick } from 'vue'
import SalesDashboard from './SalesDashboard.vue'
import SalesChart from './SalesChart.vue'
test('passes the aggregated series to the chart and opens details on select', async () => {
const wrapper = mount(SalesDashboard, {
props: { rows: [{ month: 'Jan', amount: 700 }, { month: 'Jan', amount: 500 }] },
global: { stubs: { SalesChart: true } }
})
const chart = wrapper.findComponent(SalesChart)
expect(chart.props('series')).toEqual([{ month: 'Jan', total: 1200 }])
chart.vm.$emit('point-select', { month: 'Jan' })
await nextTick()
expect(wrapper.get('[data-test="detail-title"]').text()).toContain('Jan')
})go deeper
Know that global.stubs with SalesChart: true replaces just that child with a sales-chart-stub, and that findComponent returns a wrapper with props().
Explain why props() works on a stub (it copies the props declaration and is registered against the original) and why a custom stub without declared props leaves props() empty.
Separate what the parent test proves (the data passed down, the reaction to an event) from the child's own contract, and keep one integration test so a renamed prop or event cannot slip through.
Decide which children count as stub-worthy boundaries across the codebase, such as canvas, maps or third-party widgets, and document shared stubs so teams do not each invent inconsistent fakes.
## The situation A `SalesDashboard` component fetches figures, aggregates them into a `series` array and renders `<SalesChart :series="series" @point-select="openDetail" />`. `SalesChart` wraps a canvas charting library: in a simulated DOM it is slow, may need APIs the test environment lacks, and its pixels are not what this test is about. The dashboard's job is to **compute the right series**, **pass it down**, and **react when a point is selected**. That is exactly what the test should check. ## Stub one child, keep everything else real With **Vue Test Utils** (`@vue/test-utils` 2.x for Vue 3), a targeted stub goes in the `global.stubs` mounting option: ```ts const wrapper = mount(SalesDashboard, { props: { region: 'EU' }, global: { stubs: { SalesChart: true } } }) ``` `true` asks for an **automatic stub**. Everything else in the dashboard — headings, filters, other children — still renders for real, which is why this is usually preferred over `shallow: true` when only one child is in the way. How the key is matched: - first against the name under which the dashboard registered the child (`components: { SalesChart }`) or, in `<script setup>`, the name it was imported as; - then against the component's own `name`, or the name the SFC compiler inferred from `SalesChart.vue`; - PascalCase, camelCase and kebab-case spellings all match. The stub applies anywhere in the rendered tree, not just to direct children — hence it lives under `global`. ## Asserting what the dashboard passed down The automatic stub has two properties that make it assertable: 1. It **declares the same props** as `SalesChart`, so `series` is resolved as a prop, not an attribute. 2. Vue Test Utils **registers the stub against the original**, so searching by the imported definition finds it. ```ts const chart = wrapper.findComponent(SalesChart) expect(chart.props('series')).toEqual([{ month: 'Jan', total: 1200 }]) ``` The distinction between the two finders matters here: | call | returns | can read props / emitted | |---|---|---| | `find('sales-chart-stub')` | `DOMWrapper` over an element | no — only DOM methods | | `findComponent(SalesChart)` | `VueWrapper` over the instance | yes — `props()`, `emitted()`, `vm` | The stub also prints props as attributes on `<sales-chart-stub>`, but an array or object prints as a string there. Assert through `props()`, which gives the real value. ## Driving the dashboard's reaction Because the stub instance exists, you can fire the event the real chart would emit: ```ts chart.vm.$emit('point-select', { month: 'Jan' }) await nextTick() expect(wrapper.get('[data-test="detail-title"]').text()).toContain('January') ``` This checks the dashboard's handler. It does **not** check the chart's side of the contract: the stub declares no `emits`, so it accepts any event name. If `SalesChart` renames its event, this test stays green. Cover that contract in `SalesChart`'s own test, asserting on its `emitted()` after a real interaction. ## Hand-written stubs Instead of `true`, you can pass a component object to render something simpler: ```ts global: { stubs: { SalesChart: { props: ['series'], template: '<div data-test="chart" />' } } } ``` - Declare the props you care about. When the custom stub declares none, Vue treats `series` as a fallthrough attribute on the stub's root, and `findComponent(SalesChart).props()` returns an empty object. - Keep it inert. A custom stub that grows logic becomes a second implementation to maintain. - `findComponent(SalesChart)` still finds a custom stub, because Vue Test Utils registers it against the original too. ## What this test deliberately does not prove - that the chart draws correctly — that belongs to `SalesChart`'s own tests or a browser-level test; - that the real chart emits `point-select` with that payload shape; - anything the chart would do through `provide`/`inject` or lifecycle hooks, since none of its code runs. Keeping one un-stubbed integration test of dashboard and chart together covers those seams when the chart's props or events change. ## Choosing the stub form | form | renders | props() on the stub | good for | |---|---|---|---| | `SalesChart: true` | empty `<sales-chart-stub>` | the original's props | most parent tests | | `SalesChart: { props: [...], template }` | your template | the props you declare | a parent that needs a hook element | | `shallow: true` | stubs every child | each original's props | containers made only of children | Start with `true`, and move to a custom object only when the parent's own markup or logic needs something the empty placeholder cannot provide.
- The chart is loaded with defineAsyncComponent; how does that change stubbing it with Vue Test Utils?If you stub it by the key under which the parent registered the async component, it is replaced before loading and no await is needed. If you stub it by the loaded component's own name, Vue Test Utils can only match after the loader resolves, so you await `flushPromises()` first; that route also requires the async component to define a name.
- When would you choose a hand-written stub over `true` for the chart?When the parent's markup depends on something the child renders — a `data-test` hook, a slot the parent fills, or a size the layout reads — a minimal custom template can supply it. Declare the props you assert on, keep it free of logic, and prefer `true` whenever an empty placeholder is enough.
saying these in an interview costs you the question
- Stubbing one child means switching the whole test to shallowMount.
- A stubbed child cannot be found with findComponent(OriginalComponent).
- Checking the stub's attributes is as good as checking props() for an array.
- Emitting from the stub proves the real chart emits that event name.
- A custom stub object automatically inherits the original component's props.