Contains stubbing functions for Vue components to unit test components without requiring the full component to be loaded.
When testing Vue components, you often need to test a parent component's behavior without rendering all of its child components. This library provides pre-built stub components that:
- Simplify Component Isolation: Test parent components without worrying about child component complexity or dependencies
- Verify Props Passing: Automatically display props in a testable format (e.g.,
propName-value) - Test Event Handling: Create stubs that emit events with specific payloads, allowing you to verify parent components handle events correctly
- Reduce Test Complexity: Avoid importing and configuring numerous child components in your tests
- Support Multiple Testing Libraries: Works seamlessly with both
@vue/test-utilsand@testing-library/vue
- Testing that a parent component passes the correct props to child components
- Verifying that a parent component correctly handles events emitted by children
- Testing form validation flows where child components expose validation methods
- Isolating unit tests from complex or slow-to-render child components
npm install --save-dev @sourceallies/vue-testing-library-stubsThis project is written in TypeScript.
- Node.js >= 20.0.0
- npm
npm installBuild the TypeScript source to JavaScript:
npm run buildEnsure package is not already installed in the project testing the changes.
npm uninstall @sourceallies/vue-testing-library-stubsEnsure Vue Testing Library Stubs project is using the same node version as the project testing the changes.
node -vCreate a symlink for the project testing the changes to connect to.
npm linkIn project testing the changes link the package
npm link @sourceallies/vue-testing-library-stubsThe compiled output will be in the dist/ directory.
src/
├── index.ts # Main entry point with stub functions
└── types.ts # TypeScript type definitions
The primary API is getStub, a single function that takes an options object describing everything a stub needs — props, emitted events, exposed validation, and method spies. One shape replaces the older family of getStubWithProps / getEmittingStub / getExposedValidateStub helpers (those still work but are deprecated).
import { getStub, type StubOptions } from '@sourceallies/vue-testing-library-stubs';getStub accepts either a component name string (a shorthand for the simplest stub) or a StubOptions object:
| Option | Type | Description |
|---|---|---|
componentName |
string |
Required. The name of the component to stub. |
props |
(string | object)[] |
Prop names to accept and render as propName-value. |
events |
EmittedEvent[] |
Events the stub can emit; each renders a button labelled with its name. |
validate |
boolean | { isValid?: boolean } |
Exposes a tracking validate() (and resetValidation()). true validates as valid; { isValid: false } validates as invalid. |
spies |
Record<string, Function> |
Caller-supplied mock functions merged into the stub's methods. |
Create a simple stub component for testing:
Using @vue/test-utils:
import { mount } from '@vue/test-utils';
import { getStub } from '@sourceallies/vue-testing-library-stubs';
const wrapper = mount(MyParentComponent, {
global: {
stubs: {
...getStub({ componentName: 'ChildComponent' })
}
}
});
// Verify the stub was rendered
expect(wrapper.text()).toContain('ChildComponent-stub');Using @testing-library/vue:
import { render, screen } from '@testing-library/vue';
import { getStub } from '@sourceallies/vue-testing-library-stubs';
render(MyParentComponent, {
global: {
stubs: {
...getStub({ componentName: 'ChildComponent' })
}
}
});
// Verify the stub was rendered
expect(screen.getByText('ChildComponent-stub')).toBeVisible();Shorthand:
getStub('ChildComponent')(passing just the name string) is equivalent togetStub({ componentName: 'ChildComponent' }).
Test components that pass props to child components. Props are rendered as propName-value; objects/arrays are JSON-stringified, and hyphenated names are converted to camelCase.
Using @vue/test-utils:
import { mount } from '@vue/test-utils';
import { getStub } from '@sourceallies/vue-testing-library-stubs';
const wrapper = mount(MyParentComponent, {
global: {
stubs: {
...getStub({ componentName: 'ChildComponent', props: ['title', 'count', 'config'] })
}
}
});
// Verify props are displayed (automatically converts to {propName}-{value})
expect(wrapper.text()).toContain('title-Hello');
expect(wrapper.text()).toContain('count-42');Using @testing-library/vue:
import { render, screen } from '@testing-library/vue';
import { getStub } from '@sourceallies/vue-testing-library-stubs';
render(MyParentComponent, {
global: {
stubs: {
...getStub({ componentName: 'ChildComponent', props: ['title', 'count', 'config'] })
}
}
});
expect(screen.getByText(/title-Hello/)).toBeVisible();
expect(screen.getByText(/count-42/)).toBeVisible();Each entry in events renders a button labelled with the event name. Use value to emit a single payload, or values to emit multiple arguments. Omit both to emit no arguments.
Using @vue/test-utils:
import { mount } from '@vue/test-utils';
import { getStub } from '@sourceallies/vue-testing-library-stubs';
const wrapper = mount(MyParentComponent, {
global: {
stubs: {
...getStub({
componentName: 'ChildComponent',
events: [{ name: 'save', value: { id: 123 } }]
})
}
}
});
// Trigger the event by finding the button with text 'save'
await wrapper.find('button').trigger('click');
expect(wrapper.emitted('save')).toBeTruthy();
expect(wrapper.emitted('save')?.[0]).toEqual([{ id: 123 }]);Using @testing-library/vue:
import { render, screen } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import { getStub } from '@sourceallies/vue-testing-library-stubs';
const { emitted } = render(MyParentComponent, {
global: {
stubs: {
...getStub({
componentName: 'ChildComponent',
events: [{ name: 'save', value: { id: 123 } }]
})
}
}
});
const button = screen.getByRole('button', { name: 'save' });
await userEvent.click(button);
expect(emitted()).toHaveProperty('save');
expect(emitted().save[0]).toEqual([{ id: 123 }]);const stubs = {
...getStub({
componentName: 'ChildComponent',
events: [
{ name: 'save', value: { id: 1 } }, // $emit('save', { id: 1 })
{ name: 'cancel' }, // $emit('cancel')
{ name: 'update', values: ['a', 42] }, // $emit('update', 'a', 42)
]
})
};
// Each event gets its own button, labelled with its name
const saveButton = screen.getByRole('button', { name: 'save' });
const updateButton = screen.getByRole('button', { name: 'update' });
await userEvent.click(updateButton);
expect(emitted().update[0]).toEqual(['a', 42]);Combine any options in a single call:
const wrapper = mount(MyParentComponent, {
global: {
stubs: {
...getStub({
componentName: 'ChildComponent',
props: ['title', 'isActive'],
events: [{ name: 'update', value: { newValue: 'test' } }]
})
}
}
});
expect(wrapper.text()).toContain('title-Hello');
await wrapper.find('button').trigger('click');
expect(wrapper.emitted('update')?.[0]).toEqual([{ newValue: 'test' }]);Set validate to expose a validate() method that tracks whether it was called and renders {componentName}-stub-validated-{true|false}. Pass { isValid: false } to simulate a failing validation.
Using @vue/test-utils:
import { mount } from '@vue/test-utils';
import { getStub } from '@sourceallies/vue-testing-library-stubs';
const wrapper = mount(MyFormComponent, {
global: {
stubs: {
...getStub({ componentName: 'InputComponent', validate: true }) // valid
// ...getStub({ componentName: 'InputComponent', validate: { isValid: false } }) // invalid
}
}
});
// After the parent calls validate() on the child component
expect(wrapper.text()).toContain('InputComponent-stub-validated-true');Using @testing-library/vue:
import { render, screen } from '@testing-library/vue';
import { getStub } from '@sourceallies/vue-testing-library-stubs';
render(MyFormComponent, {
global: {
stubs: {
...getStub({ componentName: 'InputComponent', validate: true })
}
}
});
expect(screen.getByText(/InputComponent-stub-validated-true/)).toBeVisible();Pass your own mock functions via spies to assert the parent invoked a child method. The library stays test-framework agnostic — you provide the spy (vi.fn(), jest.fn(), etc.) and keep a reference to assert on it. Spies are merged last, so they can also override the always-present no-op methods (validate, reset, resetValidation).
import { mount } from '@vue/test-utils';
import { vi } from 'vitest';
import { getStub } from '@sourceallies/vue-testing-library-stubs';
const reset = vi.fn();
const wrapper = mount(MyParentComponent, {
global: {
stubs: {
...getStub({ componentName: 'ChildComponent', spies: { reset } })
}
}
});
// ...after the parent calls the child's reset()
expect(reset).toHaveBeenCalled();Test components that use a child component's exposed functions. Unlike getStub, this wraps the real component (not a string-named stub), so it is not superseded by getStub.
Using @vue/test-utils:
import { mount } from '@vue/test-utils';
import { getTemplateComponentForExposedFunction } from '@sourceallies/vue-testing-library-stubs';
const props = { value: 'test', enabled: true };
const componentWithExposed = getTemplateComponentForExposedFunction(
ChildComponent,
'reset',
props
);
const wrapper = mount(componentWithExposed, {
props
});
// Click button to trigger the exposed function
await wrapper.find('button').trigger('click');Using @testing-library/vue:
import { render, screen } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import { getTemplateComponentForExposedFunction } from '@sourceallies/vue-testing-library-stubs';
const props = { value: 'test', enabled: true };
const componentWithExposed = getTemplateComponentForExposedFunction(
ChildComponent,
'reset',
props
);
render(componentWithExposed, {
props
});
// Click button to trigger the exposed function
const button = screen.getByRole('button', { name: 'reset' });
await userEvent.click(button);Creates a stub component. Call it with a component name string for the simplest stub, or with a StubOptions object to configure props, events, validation, and spies.
StubOptions:
interface StubOptions {
componentName: string;
props?: (string | object)[];
events?: EmittedEvent[];
validate?: boolean | { isValid?: boolean };
spies?: Record<string, (...args: any[]) => any>;
}
type EmittedEvent = {
name: string;
value?: any; // single value: $emit(name, value)
values?: any[]; // multiple values: $emit(name, ...values) — takes precedence over `value`
};Behavior:
- Renders
{componentName}-stuband exposes the stub under the keycomponentNamewith name{componentName}-stub. props: rendered aspropName-value. Hyphenated names are converted to camelCase, objects/arrays are JSON-stringified, arrays of functions show the function names, andundefinedprops are not rendered.events: each event renders a button labelled with its name.valuesemits multiple args,valueemits one, neither emits none.- Every object-form stub always exposes no-op
validate()(resolves{ valid: true, errors: [] }),reset(), andresetValidation()methods, so a parent that calls these over arefwon't error. validate: upgrades the no-opvalidate()to track calls (honouringisValid) and renders{componentName}-stub-validated-{true|false}.spies: merged intomethodslast, so a spy can override any default method (includingvalidate/reset/resetValidation).
Examples:
getStub('MyComponent');
getStub({ componentName: 'MyComponent', props: ['title', 'count'] });
getStub({ componentName: 'MyComponent', events: [{ name: 'save', value: { id: 1 } }] });
getStub({
componentName: 'MyComponent',
props: ['title'],
events: [{ name: 'save' }, { name: 'cancel' }],
validate: { isValid: false },
spies: { reset: vi.fn() },
});getTemplateComponentForExposedFunction(componentAlias: Component, exposedFn: string, props?: object)
Creates a wrapper component that calls an exposed function on a child component.
Parameters:
componentAlias(Component): The actual component to wrapexposedFn(string): The name of the exposed function to callprops(object, optional): Props to pass to the child component
Returns: A Vue component configuration that wraps the provided component
Features:
- Creates a button that triggers the exposed function
- Button is labeled with the function name
- Passes props to the child component
- Useful for testing components that use
expose()ordefineExpose()
Example:
const wrapper = getTemplateComponentForExposedFunction(ChildComponent, 'reset', { value: 'test' });
// Renders ChildComponent with a button that calls reset() when clicked
⚠️ The functions below remain for backward compatibility but are superseded bygetStub. They are marked@deprecatedand will show a strikethrough in editors. Prefer thegetStub({ ... })equivalent shown with each.
| Deprecated call | getStub replacement |
|---|---|
getStub('Name') |
getStub({ componentName: 'Name' }) (string form still supported as a shorthand) |
getStubWithProps('Name', 'a', 'b') |
getStub({ componentName: 'Name', props: ['a', 'b'] }) |
getEmittingStub('Name', 'save', v1, v2) |
getStub({ componentName: 'Name', events: [{ name: 'save', values: [v1, v2] }] }) |
getEmittingStubWithProps('Name', 'save', [v1, v2], 'a') |
getStub({ componentName: 'Name', props: ['a'], events: [{ name: 'save', values: [v1, v2] }] }) |
getMultiEmittingStubWithProps('Name', [{ name: 'save', value: v }], 'a') |
getStub({ componentName: 'Name', props: ['a'], events: [{ name: 'save', value: v }] }) |
getExposedValidateStub('Name', false) |
getStub({ componentName: 'Name', validate: { isValid: false } }) |
getExposedValidateStubWithProps('Name', true, 'a') |
getStub({ componentName: 'Name', props: ['a'], validate: true }) |
Emit-args difference:
getEmittingStubspreads trailing args (...emittedValues), so map those tovalues: [...].getMultiEmittingStubWithPropsuses a singlevalueper event, which maps directly tovalue. Also note a valueless event emits[undefined]in the legacy multi-stub but[]ingetStub.
Replacement:
getStub({ componentName, props: [...] })
Creates a stub component that displays its props in a testable format.
import { mount } from '@vue/test-utils';
import { getStubWithProps } from '@sourceallies/vue-testing-library-stubs';
const wrapper = mount(MyParentComponent, {
global: {
stubs: {
...getStubWithProps('ChildComponent', 'title', 'count', 'config')
}
}
});
expect(wrapper.text()).toContain('title-Hello');
expect(wrapper.text()).toContain('count-42');Replacement:
getStub({ componentName, events: [{ name, values: [...] }] })
Creates a stub component that emits a single event when a button is clicked.
import { mount } from '@vue/test-utils';
import { getEmittingStub } from '@sourceallies/vue-testing-library-stubs';
const wrapper = mount(MyParentComponent, {
global: {
stubs: {
...getEmittingStub('ChildComponent', 'save', { id: 123 })
}
}
});
await wrapper.find('button').trigger('click');
expect(wrapper.emitted('save')).toBeTruthy();
expect(wrapper.emitted('save')?.[0]).toEqual([{ id: 123 }]);getEmittingStubWithProps(componentName: string, emittedEvent: string, emittedValues: any[], ...props: (string | object)[])
Replacement:
getStub({ componentName, props: [...], events: [{ name, values: [...] }] })
Creates a stub component that combines props display and event emission.
import { mount } from '@vue/test-utils';
import { getEmittingStubWithProps } from '@sourceallies/vue-testing-library-stubs';
const wrapper = mount(MyParentComponent, {
global: {
stubs: {
...getEmittingStubWithProps('ChildComponent', 'update', [{ newValue: 'test' }], 'title', 'isActive')
}
}
});
await wrapper.find('button').trigger('click');
expect(wrapper.emitted('update')?.[0]).toEqual([{ newValue: 'test' }]);getMultiEmittingStubWithProps(componentName: string, events: EmittedEvent[], ...props: (string | object)[])
Replacement:
getStub({ componentName, props: [...], events: [{ name, value }] })
Creates a stub component that can emit multiple different events.
import { mount } from '@vue/test-utils';
import { getMultiEmittingStubWithProps, type EmittedEvent } from '@sourceallies/vue-testing-library-stubs';
const events: EmittedEvent[] = [
{ name: 'save', value: { id: 1 } },
{ name: 'cancel' },
{ name: 'delete', value: { id: 1 } }
];
const wrapper = mount(MyParentComponent, {
global: {
stubs: {
...getMultiEmittingStubWithProps('ChildComponent', events, 'title', 'status')
}
}
});
const buttons = wrapper.findAll('button');
await buttons[0].trigger('click'); // Triggers 'save'
expect(wrapper.emitted('save')?.[0]).toEqual([{ id: 1 }]);Note: This legacy function emits
[undefined]for a valueless event, whereasgetStubemits[](no arguments). Keep that in mind when migrating assertions.
Replacement:
getStub({ componentName, validate: true })(orvalidate: { isValid: false })
Creates a stub component with an exposed validate() method for testing form validation.
import { mount } from '@vue/test-utils';
import { getExposedValidateStub } from '@sourceallies/vue-testing-library-stubs';
const wrapper = mount(MyFormComponent, {
global: {
stubs: {
...getExposedValidateStub('InputComponent', true) // true = validation passes
}
}
});
expect(wrapper.text()).toContain('InputComponent-stub-validated-true');getExposedValidateStubWithProps(componentName: string, isValid: boolean = true, ...props: (string | object)[])
Replacement:
getStub({ componentName, props: [...], validate: true })
Creates a stub component that combines validation testing with props display.
const stub = getExposedValidateStubWithProps('InputComponent', true, 'value', 'label');MIT