Vue-test-utils: Automatically stub nested components with their names instead of `<!---->`

Created on 4 Feb 2018  Â·  23Comments  Â·  Source: vuejs/vue-test-utils

What problem does this feature solve?

In my workflow it is a pretty common use case to have a unit test that checks if some nested component is being rendered conditionally.

For example i have a component with a template like this (I use pug for templating, just so there won't be any confusion):

.container
  MyCustomComponent(v-if="condition" :title="cog.title")

And I want to have a unit test that makes sure that MyCustomComponent is only rendered when that condition is satisfied.

I'm coming from React background, where enzyme's shallow render passes component's display name by default. So if I would shallow render the setup above - the wrapper.html() would look something like this (if the v-if condition was met):

<div class="container">
  <MyCustomComponent title="something from cog.title" />
</div>

But in vue-test-utils the result would actually look like this:

<div class="container">
  <!---->
</div>

There is currently an option to supply a stubs object to shallow render method, but that requires you to manually write a list of components and their respective stubs, which is not time-efficient.

Currently I wrote a simple stub-generator, which acts somewhat like identity-object-proxy, and basically looks at the list of components in options of the component we want to render, and generates an object of stubs.

That allows me to achieve a result like this:

<div class="container">
  <mycustomcomponent title="something from cog.title" />
</div>

Which is basically what I need.

I can see more people that might want this kind of behaviour to speed up the unit testing, as this is a pretty common approach in other systems.

My stubs generator code for context:

// Using custom proxy to automatically stub with proper component names
const idObj = new Proxy({}, {
  get: function getter(target, key) {
    return `<${key} />`;
  },
});

// Generate the object of stubs with proxy
const genStubs = component => Object.keys(component.options.components).reduce((acc, comp) => {
  acc[comp] = idObj[comp];
  return acc;
}, {});

Pretty basic, but it allows me to simply use wrapper.find('<component name>'); to conditionally check if something was rendered, without manually supplying the stubs object every time, since I have a custom wrapper around shallow that passes my generator to the stubs option.

What does the proposed API look like?

I'm not sure if this should be the default behaviour, since people might rely on current way of stubbing in the existing code, but I can see something like a flag "useNamedStubs" for shallow options, or something similar.

feature request intend to implement

Most helpful comment

I've written a stub generator that works with jest and vue-test-utils:

export function generateStubs(cmp) {
  return Object.values(cmp.components).reduce((stubs, stubCmp) => {
    const dashName = stubCmp.name
      .replace(/([a-z])([A-Z])/g, '$1-$2')
      .toLowerCase();
    stubs[dashName] = {
      render(createElement) {
        return createElement(dashName, this.$slots.default);
      },
    };
    return stubs;
  }, {});
}

This requires all components to have their name option set, be it in camel case or kebab case (see https://vuejs.org/v2/api/#name). It creates a stub component render function that renders the element and its children (if you're passing children you can see those in your snapshot as well).

Use it like this:

// components
export default {
  name: 'MyComponent',
  components: {
    FooBar,
  },
}

export default {
  name: 'FooBar',
}

// test
const component = shallow(MyComponent, {
  stubs: generateStubs(MyComponent),
});

All 23 comments

This is a great idea! We could make it an option, and also have it as an option you can set in the config, so you only need to set it once:

VueTestUtils.config.shallowStubsAsText = true

Would you like to add a PR? I'm happy to if you're not able to

By the way, are you aware you can check a component has been rendered with shallow?

import MyCustomComponent from '~/MyCustomComponent.vue'

const wrapper = shallow(TestComponent)
wrapper.find(MyCustomComponent).exists()

This is sort of a clone of #28. When we add this feature, we can close #28

Sadly, I have too much on my hands atm to make a PR.

I was unaware that you can check if a component is included that way, but I think the text stubs will still help when you’re debugging stuff (logging out wrapper.html()) and it will make it a generally more streamlined experience.

Thanks for a quick response! :)

Actually, I think I'll look into it. Have some more time this week. Shouldn't be that complicated

So, yeah, that took more than a week :) I played around with the concept, and I'm a bit concerned about all the warnings in the console about using non-registered components with an approach like that. Was unable to get around them.

I will try to make a PR sometime this week, so maybe someone else can take a look as well.

This feature will be useful for snapshot testing also.
@orels1 could you please explain in more details how you use your "stubs generator" for render stubs as string?

Hey @ilyaztsv you want me to share my current solution?
If so, you can find it used here: https://github.com/orels1/v3.cogs.red/blob/feature/%2326/unit-tests/test/unit/utils.js

The above is a set of handy utils I use to have better time testing. This approach drops a lot of warnings about "unregistered components", but if those are not a big deal for you - this setup works quite well. I was emulating the way shallow rendering works in enzyme

@orels1 You can ignore custom elements by adding them to the ignoredElements array—https://vuejs.org/v2/api/#ignoredElements. That should stop warnings

@orels1 thanks for your solution. I'll try it with jest today.

@orels1 @eddyerburgh Unfortunately, "stub-generator" solution does not work for me. May be because of I use jest + vue-test-utils.
I've already created the issue for that #465

@ilyaztsv my solution uses jest as well

@orels1 yes. I was wrong about jest. I wrote about the reason here #465

I've written a stub generator that works with jest and vue-test-utils:

export function generateStubs(cmp) {
  return Object.values(cmp.components).reduce((stubs, stubCmp) => {
    const dashName = stubCmp.name
      .replace(/([a-z])([A-Z])/g, '$1-$2')
      .toLowerCase();
    stubs[dashName] = {
      render(createElement) {
        return createElement(dashName, this.$slots.default);
      },
    };
    return stubs;
  }, {});
}

This requires all components to have their name option set, be it in camel case or kebab case (see https://vuejs.org/v2/api/#name). It creates a stub component render function that renders the element and its children (if you're passing children you can see those in your snapshot as well).

Use it like this:

// components
export default {
  name: 'MyComponent',
  components: {
    FooBar,
  },
}

export default {
  name: 'FooBar',
}

// test
const component = shallow(MyComponent, {
  stubs: generateStubs(MyComponent),
});

@AlbertBrand unfortunately, it is still cause RangeError: Maximum call stack size exceeded for the case in https://github.com/ilyaztsv/jest-and-vue-test-utils-stubs repo.

you can see #465 for more details.

@ilyaztsv Interesting, I tried some things in your repo and found the issue. If you change line 13 in my-component.js to ChildComponent (so don't give it a dashed key), your test succeeds. It probably has to do with a clash of names.

@AlbertBrand thanks for the reply! It did help :)

Seems like it also possible to auto-generate stubs when registering plugin like Vuetify.

@AlbertBrand Thanks for sharing your tips. Unfortunately, https://github.com/vuejs/vue-test-utils/issues/410#issuecomment-375894160 did not work in my environment while components for typescript user is wrapped with Vue.extend().
Additionally, the tag name generated from the function was different from what I specified to the components attribute of a parent component.

So I modified it as below and works well. In case someone faced the same problem.

export function generateStubs(component: any) {
  const children = component instanceof Function
    ? component.extendOptions.components
    : component.components;
  const reducer = (accumulator: {[key: string]: any}, value: string) => {
    const lhs = value[0];
    const rhs = value.substr(1);
    const name = (lhs + rhs.replace(/([A-Z])/g, '-$1')).toLowerCase();
    accumulator[name] = {
      render(createElement) {
        return createElement(name, this.$slots.default);
      },
    };
    return accumulator;
  };
  return !!children ? Object.keys(children).reduce(reducer, {}) : undefined;
}

Tested with

@Component
export default class MaterialIconView extends Vue {
  // ...
}

@Component({
  components: {
    'x-material-icon': MaterialIconView,
  },
})
export default class AccountMenuView extends Vue {
  // ...
}

const wrapper = shallow(AccountMenuView, {
  stubs: generateStubs(AccountMenuView),
});

expect(wrapper.html()).toMatchSnapshot();
// Produce below
//
// <div class="account-menu">
//   <div class="label"><span>[email protected]</span>
//     <x-material-icon icon="person"></x-material-icon>
//   </div>
//   <!---->
// </div>
//

Unfortunately, above stub seal events, attributes, or etc so it is really difficult to test the behavior.

Is someone working on this actually?

Not currently.

If you'd like to work on it, I would be happy to review a PR :)

@AlbertBrand 's solution looks well to me.

I personally have been using a generalized mock-component that just displays a fake element and renders all its slots and children and most of the props and attrs as normal DOM attributes and it works quite well for snapshots when used in combination with jest-serializer-html.

Here is the gist

Maybe it's helpful for someone

Was this page helpful?
0 / 5 - 0 ratings