When to stub child components and when to render them fully
shallowMount replaces every child component with a stub and only renders the component under test, while mount builds the full component tree. Both functions from Vue Test Utils solve different testing problems, and mixing them up either leaves tests checking too much at once or makes them break on internal details of unrelated child components.
Table of Contents
- 1. What shallowMount and mount actually do differently
- 2. When shallowMount is the better fit
- 3. When mount is the right choice
- 4. Pitfalls with slots
- 5. Pitfalls with provide/inject
- 6. A practical example: combining both approaches in one test file
- 7. Fine-grained control over stubs
- 8. Performance in deep component trees
- 9. A practical decision guide
- 10. Summary
- 11. FAQ
1. What shallowMount and mount actually do differently
Both functions from @vue/test-utils render a Vue component inside an isolated test environment and return a wrapper you can use to inspect the rendered tree, trigger events, and change props. The key difference is what happens to child components. mount() renders every child component fully, including its own setup() logic, lifecycle hooks, and any further nesting inside it. shallowMount(), on the other hand, automatically replaces every child component with a simple stub that only renders the component name as a placeholder tag, without executing the actual implementation.
That sounds like a purely technical detail, but it has direct consequences for what a test actually proves. A test using mount() implicitly exercises the behavior of every child component too, because they genuinely run. A bug deep inside a nested child component can then fail a test that was only meant to cover the parent. shallowMount() deliberately cuts that chain and makes the test indifferent to anything happening inside child components, as long as their public interface through props and events behaves correctly.
import { describe, it, expect } from 'vitest'
import { shallowMount, mount } from '@vue/test-utils'
import ProductCard from '@/components/ProductCard.vue'
describe('ProductCard', () => {
it('renders child components as stubs (isolated)', () => {
const wrapper = shallowMount(ProductCard, {
props: { name: 'Teapot', price: 29.9 },
})
// PriceTag is not actually executed, just rendered as a stub tag
expect(wrapper.findComponent({ name: 'PriceTag' }).exists()).toBe(true)
expect(wrapper.text()).toContain('Teapot')
})
it('renders the full tree including PriceTag logic', () => {
const wrapper = mount(ProductCard, {
props: { name: 'Teapot', price: 29.9 },
})
// Here the actual formatting logic inside PriceTag is exercised
expect(wrapper.text()).toContain('$29.90')
})
})
2. When shallowMount is the better fit
shallowMount fits classic unit tests where exactly one component is under scrutiny and its child components are interchangeable implementation details. Typical candidates are container or layout components that orchestrate many children but contain little logic of their own beyond passing props through and reacting to events. Testing these with mount makes the test suddenly depend on every child working correctly, which obscures the actual cause whenever a test turns red.
A second benefit shows up in deep component trees: every additional level that mount actually renders costs time, especially when child components themselves load asynchronous data, start timers, or run expensive computations inside setup(). Across a test suite with several hundred component tests, that difference quickly adds up to noticeably longer CI run times. shallowMount keeps per-test execution time low and constant, because only one component level ever actually runs, regardless of how deep the real tree is in production.
3. When mount is the right choice
mount is indispensable whenever a test needs to verify the actual interplay between multiple components, for example whether a click inside a child component correctly propagates up to the parent and triggers a visible change there. Integration-style tests that cover a complete form flow across several field components make little sense with stubbed children, because the interaction between the parts is exactly what's under test.
For small, purely presentational components without meaningful children, mount also tends to feel simpler, since no extra thought about stubbing strategy is needed. A useful rule of thumb: the closer a test gets to a real end-to-end scenario within the component world, the more mount pays off. The more isolated a single responsibility is supposed to be, the more shallowMount wins out.
4. Pitfalls with slots
A common stumbling block with shallowMount involves slots. When a child component is stubbed, Vue Test Utils by default does not render any slot content that this child component would normally insert in its place, because the stub doesn't know about the internal <slot /> definition. A test checking whether certain text becomes visible through a default slot inside a child component may fail with shallowMount even though the component works correctly in production.
The shallow: false option for individual child components, or a targeted stubs object with { template: '<slot />' } for the affected child, solves this precisely without switching to full mount for the entire test. It's worth internalizing that slot behavior isn't an implementation detail of a child component but part of its public contract, and tests that specifically check it need to be configured accordingly.
5. Pitfalls with provide/inject
Provide/inject chains create a second, more subtle problem with shallowMount. Since child components never actually run, it's never verified whether an injected value arrives there correctly and is processed as expected. A bug in the injection chain, such as a wrong injection key or a missing default value, stays invisible with shallowMount, because the stub never reaches the real child component's inject() call.
Anyone using provide/inject as a central communication path between parent and child components, for instance a shared form context object, should specifically test that path with mount, or at least with one deliberately unstubbed child. A sensible strategy is to cover pure structure and props propagation with shallowMount and verify the actual provide/inject communication in a separate, smaller integration test with mount, instead of mixing both concerns into one overloaded test.
6. A practical example: combining both approaches in one test file
In practice, shallowMount and mount aren't mutually exclusive; they complement each other within the same test file depending on the test case. For a form component with several field sub-components, shallowMount quickly verifies whether all expected fields render at all and whether props are passed through correctly. For the actual submit flow, where a click inside a field is meant to eventually call the parent's submit handler with the right values, mount is the better fit, since it exercises the real interaction between components.
This combination keeps the overall test suite fast, because the majority of tests that only settle structural or props-related questions get by with the cheaper shallowMount, while the few genuinely interaction-critical tests deliberately take the more expensive but more meaningful mount path. Making this distinction consciously from the start avoids both over-engineered stub configurations and needlessly slow test suites.
7. Fine-grained control over stubs
Vue Test Utils lets you break shallowMount's all-or-nothing behavior on purpose. The global.stubs option lets you explicitly decide, per child component, whether it should be stubbed or not, for example stubs: { PriceTag: false } to fully render exactly that one component despite using shallowMount overall. Conversely, with mount, stubs: { HeavyChart: true } lets you exclude a single, particularly expensive child component without switching the whole test file back to shallowMount.
This fine control is especially valuable for components that pull in one heavyweight dependency, such as a charting library or a map component, whose rendering in a test environment provides little value and only costs time. Instead of shaping the entire test strategy around that one component, you can stub just it while keeping the rest of the tree as realistic as needed.
8. Performance in deep component trees
In design-system components that are themselves composed of several layers of sub-components, for example a date picker that internally assembles a calendar grid, month navigation, and an input field as separate components, the difference between shallowMount and mount becomes especially noticeable. A mount test at the top level might end up rendering dozens of child components even though the test only meant to verify a single prop being passed through.
In CI environments with several thousand tests, this unnecessary rendering work adds up to a measurable slowdown of the entire pipeline. A pragmatic approach is to test complex, deeply nested components with shallowMount as the default, and reach for mount only where a multi-level interaction genuinely needs verification. Many teams even codify this as a convention to avoid re-litigating the choice on every single test.
9. A practical decision guide
As a rough guide, ask what a test is actually meant to prove. If it's about the internal logic of a single component, conditional rendering, props handling, or local state, shallowMount is almost always the right, faster choice, because everything outside that boundary is deliberately hidden. If it's about the interplay between multiple components, especially through events, slots, or provide/inject, mount is necessary, because that interplay simply doesn't happen otherwise.
A good code review looks not only at whether a test is green, but also at whether the chosen mount strategy matches the actual testing intent. A mount test that really only checks an isolated property is needlessly slow and fragile against changes in child components. A shallowMount test that's meant to check interaction between components, on the other hand, gives a false sense of safety, because the critical connection is never actually exercised.
| Criterion | shallowMount | mount |
|---|---|---|
| Child components | rendered as stubs | fully executed |
| Test speed | faster, constant | depends on tree depth |
| Slot content | not visible by default | fully visible |
| Provide/inject | not really exercised | actually exercised |
| Typical use | isolated unit tests | integration tests |
Mironsoft
Vue architecture, Composition API, and Nuxt performance
Vue applications that don't get more complicated with every feature?
We review existing Vue and Nuxt projects for unstructured composables, unnecessary reactivity, and bloated bundles, then build an architecture that absorbs new features without making the codebase harder to follow.
Architecture Review
Checking composables, state management, and component structure for maintainability.
Performance Audit
Systematically optimizing reactivity overhead, bundle size, and Nuxt rendering strategy.
Nuxt Integration
Building robust, type-safe SSR/SSG setup and API integration.
10. Summary
shallowMount vs. mount: key takeaways at a glance
Isolation
shallowMount stubs all child components automatically
Completeness
mount renders the full component tree
Slots
slot content from stubbed children stays invisible with shallowMount
Fine control
global.stubs allows targeted exceptions for individual components