vue-test-utils: Named slots' content is not generated for stubbed components via shallowMount

What problem does this feature solve?

Higher-order components make use of named slots. It’s possible to test higher-order components via shallowMount because named slot content can be mocked, but it’s not possible to test components which consume higher-order components because their generated named slot output is not available in the stubbed content.

https://github.com/vuejs/vue-test-utils/pull/782 added support for outputting default slot content, but not named slots.

To be clear, an example of a component which needs this support is:

<!-- myComponent.vue -->
<template>
  <HigherOrderComponent foo="bar">
    default slot content
    <template v-slot="header">header slot content</template>
    <template v-slot="footer">footer slot content<HigherOrderComponent foo="waldo"/></template>
  </HigherOrderComponent>
</template>

Currently, this is stubbed out to only:

<higherordercomponent-stub foo="bar">default slot content</higherordercomponent-stub>

The end user experience should be that components which generate named slot content “just work” when testing with shallowMount.

What does the proposed API look like?

The API would not change.

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 2
  • Comments: 18 (8 by maintainers)

Most upvoted comments

For those looking for a solution now, manually stubbing out the component seems to render the child slots:

const wrapper = shallowMount(MyComponent, {
  stubs: { HigherOrderComponent }
}

expect(wrapper.text()).toContain('header slot content');
expect(wrapper.text()).toContain('footer slot content');

It wasn’t just a case of “I didn’t merge it”. Looks like there was some conflict and failing tests here which were not resolved: https://github.com/vuejs/vue-test-utils/pull/1309

I think we need to support both behaviors to make this work for everyone. Some people will expect a true shallow mount, others want stubbed components to render slots.

I am pretty focused on getting v1 of this lib released, vue-jest 4 and 5, and VTU next, so I cannot work on this right now. If you (or anyone else reading this) would like to take a stab at this (or anyone else) feel free to do so. What I’d like to see:

  • stub all the children as the default
  • support rendering slots of stubbed children. I don’t know what the API should look like, perhaps
import { config } from '@vue/test-utils'

config.shallowMount = {
  renderSlots: true
}

Or

shallowMount(Foo, {
  stubbedSlots: true
})

I don’t think it makes sense as a mounting option, since those are shared with mount, though.

Another alternative would be a shallowWithStubs method.

Either way, the API won’t impact the implementation. Does this sound appropriate? I think this will let everyone write their tests however they like.

This may work but you end up stubbing so much you aren’t really testing anything. The best way to test slots is by using mount, you’ll get the same behavior as production and avoid writing tests that give you false confidence.

If you have 1 million unique components then your component is not testable. Write your code in a testable manner.

I said “for the sake of example”, the focus here is not the number of components. You said “just stub out the troublesome component”, and I’m saying that it could be 2, 5, a dozen, or any arbitrary number of components that need to be stubbed out when using mount, with the worst case being having to stub out every other component except for the one being tested. For large apps with deep trees, this can lead to stubbing out large parts of the app just to write a “unit test”, which is starting with an integration test and stubbing out everything that’s causing problems until it effectively becomes a unit test.

Clearly I am not the target user for this feature, so I can’t fully understand.

The user story is, “as a developer writing a unit test, I want to verify that my component is passing the proper data to a child component.” Data is defined as anything and everything that can be passed to the child component, which AFAIK is props, attributes, and templates. So given this example:

<my-component>
  <!-- other stuff that my-component does with its data -->
  <child-component :p1="data.p1" :p2="data.p2" data-prefix="prefix">
    <template>{{ data.defaultSlot }}</template>
    <template #named>{{ data.namedSlot }}</template>
  </child-component>
</my-component>
it('test my component', () => {
  const data = {
    p1: 'p1',
    p2: 'p2'
    defaultSlot: 'default slot text',
    namedSlot: 'named slot text'
  }
  const wrapper = shallowMount(MyComponent, {
    propsData: { data });
  })
  const child = wrapper.find(ChildComponent);

  // We can test for props on child stubs.
  expect(child.props('p1')).toBe(data.p1);
  expect(child.props('p2')).toBe(data.p2);
  // We can test for attributes on child stubs.
  expect(child.attributes('data-prefix')).toBe('prefix');
  // We can test for the default slot on child stubs.
  expect(child.text()).toContain(data.defaultSlot);

  // We CANNOT test for named slots on child stubs.
  expect(child.text()).toContain(data.namedSlot);
}

If you feel strongly about this issue, you are more than welcome to attempt to implement the above behavior in this code base, too.

It’s already been done, but you closed it: https://github.com/vuejs/vue-test-utils/pull/1309 From your comment in the PR:

I think stubbed children should not render their slots.

To reiterate the above example:

We can test that props are properly passed to children using .props(). We can test that attributes are properly passed to children using .attributes(). We can test that the default slot is properly passed to children using .text() or .html().

Without the stubs: { Component } workaround, we cannot test that named slots are passed properly. Whether stubs should render content inside them is really not the point here, it’s that there’s no other way to test slots otherwise in a unit test. If there was something like childWrapper.templates(), then I’m all for stubbing out the child completely, but until that happens, checking childWrapper.text() or childWrapper.html() remains the only way to verify slots. Using mount is not a solution because, once again, it turns a unit test into an integration test.

Either way, I am way pasting trying to push this issue. Let’s move on.

Sure, my original comment was simply to provide a solution that works today. This issue has been reported multiple times and even has a PR for it, so it’s definitely important to at least some of us, and we can’t wait around for the Vue 3 integration.

Why people are obsessed with isolating components is beyond me.

I already addressed that, but to quote @souldzin, who put it much better than I can: https://github.com/vuejs/vue-test-utils/issues/1498#issuecomment-612988985

mount and shallowMount are different levels of testing, each with their own pro’s and con’s. TDD’ing from the perspective of a user with mount is awesome (and my personal preference), but it isn’t free. There’s some application states which are difficult to get to in full integration (e.g. step 3 of a highly interactive single page form). Even if I build a test helper to get there, my tests suite is going to be doing a lot of redundant work to trigger this state in an integrated environment. This is where the profitability of shallowMount unit tests can come in.

What do I get? <foo-stub><bar-stub><qux-stub></bar-stub></foo-stub>?

Yes.

I still don’t really see what’s so bad about using stubs or jest.mock

For the sake of example, you have 1 million unique child components, each of which does an AJAX call in their created() hook that takes 2 seconds to complete. Do you stub every one out? And after you do, what’s the difference between mount and shallowMount if you’ve stubbed everything out except the one component you’re testing?

To be clear, to properly test a component, its shallowMount rendering should be something like this:

<higherordercomponent-stub foo="bar">
  default slot content
  <template-stub v-slot="header">header slot content</template-stub>
  <template-stub v-slot="footer">footer slot content<higherordercomponent-stub foo="waldo"/></template-stub>
</higherordercomponent-stub>

Not simply this:

<higherordercomponent-stub foo="bar">
  default slot content
  header slot content
  footer slot content<higherordercomponent-stub foo="waldo"/>
</higherordercomponent-stub>

but you end up stubbing so much

That’s the intention. To use OP’s example, I’m writing a unit test for myComponent.vue, so I want HigherOrderComponent to be stubbed out. I also want to test that myComponent is sending down the correct template data to HigherOrderComponent.

Right now, the default template is being rendered in the stub with shallowMount, but named templates are not. This doesn’t make sense. Manually stubbing out HigherOrderComponent fixes it so that named templates are rendered in the stub as well.

Using mount means that the unit test is no longer a unit test. If myComponent was the root component, by using mount the entire app is instantiated. This is explicitly mentioned in the official docs:

https://vue-test-utils.vuejs.org/guides/common-tips.html

In unit tests, we typically want to focus on the component being tested as an isolated unit and avoid indirectly asserting the behavior of its child components.

In addition, for components that contain many child components, the entire rendered tree can get really big. Repeatedly rendering all child components could slow down our tests.

Vue Test Utils allows you to mount a component without rendering its child components (by stubbing them) with the shallowMount method

I have closed the PR that supposedly solved this problem.

We will revisit shallowMount and how it should render things for the Vue 3 integration. At this point, I don’t think we will change how shallowMount is working.

I will make a new issue when we revisit shallowMount for v3 integration and invite everyone’s input that point.