skip to content

In Vue Test Utils, what does shallowMount actually do to child components, and what can a shallow test no longer check compared with mount?

level: middleimportance: must knowfreq 62%

answer

  1. same function, one option set
  2. children become placeholder elements
  3. stub keeps the child's props
  4. slots vanish inside stubs
  5. opt one child back out

basics

~20 s

shallowMount is mount with shallow: true: every child component becomes an automatic stub rendering an empty <name-stub> element that keeps the child's props but runs none of its code. Children's markup, slot content and real events go untested.

solid answer

~40 s

`shallowMount(Comp, opts)` is literally `mount(Comp, { ...opts, shallow: true })`. The component under test renders for real, but every child component becomes a stub named after it — `<UserAvatar>` renders as an empty `<user-avatar-stub>` with the props it received shown as attributes. The stub copies the child's props declaration, so `findComponent(UserAvatar).props()` still works, but the child's setup, lifecycle hooks and template never run. What you lose: the child's markup, the integration between parent and child (real emitted events, `provide`/`inject` use), and **slot content** you pass into children — stubs render no slots unless `global.renderStubDefaultSlot` is `true`, and even then only the default slot. You can keep one child real with `global: { stubs: { UserAvatar: false } }`. `mount` renders the whole tree, which is slower but checks what the user actually sees.

code

ts · 16 lines
ts
import { mount, shallowMount } from '@vue/test-utils'
import ProfileCard from './ProfileCard.vue'
import UserAvatar from './UserAvatar.vue'

test('shallow: children become stubs that keep their props', () => {
  const wrapper = shallowMount(ProfileCard, { props: { userId: 7 } })

  // <user-avatar-stub size="48"></user-avatar-stub>
  expect(wrapper.find('user-avatar-stub').exists()).toBe(true)
  expect(wrapper.findComponent(UserAvatar).props('size')).toBe(48)
})

test('full mount: the real avatar renders its image', () => {
  const wrapper = mount(ProfileCard, { props: { userId: 7 } })
  expect(wrapper.find('img').exists()).toBe(true)
})

go deeper

for a junior

Remember that shallowMount equals mount with shallow: true and that children show up as empty name-stub elements.

for a middle

Explain what a stub keeps (the props declaration, findability by the original component) and what it drops (setup, hooks, template, slots), and name the two escape hatches: stubs set to false and renderStubDefaultSlot.

for a senior

Judge each suite by what shallow rendering hides: slot content, real child events and runtime collaboration. Prefer a full mount with a single targeted stub when only one child is in the way.

for a principal

Set a default for the codebase, usually full mount with explicit stubs at expensive boundaries, and make shallow rendering an exception that code review has to justify.

## Two entry points, one mechanism In **Vue Test Utils 2** (`@vue/test-utils`, for Vue 3) there is only one real mounting function. `shallowMount` is defined as `mount(component, { ...options, shallow: true })`, so everything below applies equally to `shallowMount(Comp)` and `mount(Comp, { shallow: true })`. Vue Test Utils implements stubbing with Vue's `transformVNodeArgs` hook: before each component vnode is created, it looks at the component type and may swap it for a **stub** — a stand-in component. With `shallow: true`, every component vnode below the root is swapped. The root (the component you mounted) is never stubbed, and plain HTML elements are untouched. ## What an automatic stub looks like An automatic stub is generated per child and has four properties that matter in tests: 1. **Its tag** is the child's name, hyphenated, plus `-stub`. A child registered or imported as `UserAvatar` renders `<user-avatar-stub>`. 2. **It copies the child's props declaration**, so the props the parent passes are resolved as props, and `findComponent(UserAvatar).props('size')` returns the real value. For readability, the stub also renders those props as attributes (functions show as `[Function]`). 3. **It runs none of the child's code**: no `setup`, no lifecycle hooks, no fetch in `onMounted`, no template. 4. **It renders no slots** by default. Content the parent places between `<UserAvatar>` tags is simply not rendered. Vue Test Utils also remembers which original each stub stands for, so `findComponent(UserAvatar)` finds the stub by the imported definition. ## Controlling shallow rendering | goal | option | |---|---| | stub every child | `shallow: true` or `shallowMount()` | | keep one child real under shallow | `global: { stubs: { UserAvatar: false } }` | | render default-slot content inside stubs | `global: { renderStubDefaultSlot: true }` | | stub only one child under full mount | `global: { stubs: { UserAvatar: true } }` | `renderStubDefaultSlot` covers the **default slot only**; named and scoped slots are still not rendered by a stub. The guide sets it through `config.global.renderStubDefaultSlot` in hooks, and the mounting option of the same name under `global` does it for one mount. ## What a shallow test cannot check Because the children are empty shells, a shallow test is blind to: - **the children's own output** — text, classes, accessible names rendered by child components; - **slot content you hand to children** — a `<BaseButton>Save</BaseButton>` shows no "Save" under shallow; - **the real event contract** — a stub declares no emits, so nothing checks that the child really emits the event name the parent listens for; - **provide/inject and other runtime collaboration** between parent and child, since the child's setup never runs. What it still checks well: the parent's own markup, which children it chooses to render, with which props, and how it reacts when a child event arrives. ## When each fits - **`mount`** exercises the component the way users meet it and catches integration breaks; it costs more setup (plugins, providers, network mocks for children) and runs slower. - **`shallow`** isolates a parent whose children are expensive or side-effectful, and keeps failures local. The Vue Test Utils guide recommends `mount` plus a targeted `global.stubs` entry when only one or two children are in the way, because the more you stub, the less production-like the test becomes. ```ts const wrapper = mount(ProfileCard, { shallow: true, global: { stubs: { UserName: false } } // keep the one child this test is about }) expect(wrapper.findComponent(UserAvatar).props('size')).toBe(48) expect(wrapper.find('user-avatar-stub').exists()).toBe(true) ``` ## Pitfalls that come up in review - **Asserting on stub attributes instead of props.** `<user-avatar-stub>` prints props as attributes, but objects and arrays print as strings; read them through `findComponent(...).props()`. - **Assuming a stubbed child's side effects happened.** If the parent relies on a child fetching data or emitting on mount, a shallow test will not see it — that code never runs. - **Snapshotting shallow output.** The markup is mostly `-stub` tags, so a snapshot mainly records component names and prop values, and changes whenever a child is renamed. - **Mixing `shallow` with a global stub list.** Entries in `global.stubs` still apply under `shallow`; a custom stub object there wins over the automatic stub, and `false` there is the only way back to a real child.

  • Under shallow mounting, the parent passes `<BaseButton>Save</BaseButton>` and the test asserts the text "Save"; why does it fail, and what are the fixes?
    The BaseButton stub renders no slots, so "Save" never reaches the DOM. Either keep that child real with `global: { stubs: { BaseButton: false } }`, or set `renderStubDefaultSlot: true` under `global` so stubs render their default slot. The second fix does not help for a named or scoped slot.
  • How does Vue Test Utils decide the name used for a stub's tag and for matching a `global.stubs` key?
    It first uses the name under which the parent registered or, in `<script setup>`, imported the child; failing that, the component's own `name` or the name the SFC compiler inferred from the file. Keys match in PascalCase, camelCase or kebab-case, and the stub tag is that name hyphenated plus `-stub`.

A shallow mount is a rehearsal where every supporting actor is replaced by a stand-in wearing the actor's name card. The stand-in accepts the same cues (props) and can be pointed at, but never says a line, so any lines you wrote for them (slot content) are never heard.

saying these in an interview costs you the question

  • shallowMount is a separate renderer with different lifecycle rules from mount.
  • A stubbed child still runs its setup and onMounted, it just hides its markup.
  • Stubs drop props, so you cannot assert what the parent passed to a child.
  • renderStubDefaultSlot makes stubs render every slot, including named ones.
  • Shallow tests catch a child's renamed event because the parent still listens for it.