Unit Testing a Vue.js Functional Component that Returns Multiple Root Nodes

I've been learning Vue.js recently by building a small app. Part of my app displays information using a table, so I extracted out the row into its own functional component using a render function that returns multiple root nodes. That worked great except when it came time to test the component.

<!-- Cell.vue -->
<script>
export default {
  functional: true,
  props: ['cellData'],
  render: function (h, context) {
    return [
      h('td', context.props.cellData.category),
      h('td', context.props.cellData.description)
    ]
  }
}
</script>

The first tricky part was that I discovered that you can't use propsData to pass props to a functional component. Instead you need to pass a context object that has a props property.

Next I tried to shallowMount the component as I typically do, but that resulted in a warning: [Vue warn]: Multiple root nodes returned from render function. Render function should return a single root node.

// Cell.spec.js
import { shallowMount } from '@vue/test-utils'
import Cell from '@/components/Cell'

wrapper = shallowMount(Cell, {
  context: {
    props: {
      cellData {
        category: 'foo',
        description: 'bar'
      }
    }
  }
}); // [Vue warn]: Multiple root nodes returned from render function. Render function should return a single root node.

I next tried wrapping the component in a single <div> to create a single root node. That didn't work either though as I got an error: [vue-test-utils]: mount.context can only be used when mounting a functional component.

// Cell.spec.js
import { shallowMount } from '@vue/test-utils'
import Cell from '@/components/Cell'

wrapper = shallowMount('<div><Cell></div>', {
  context: {
    props: {
      cellData {
        category: 'foo',
        description: 'bar'
      }
    }
  }
}); // [vue-test-utils]: mount.context can only be used when mounting a functional component.

At this point I didn't know what to do so (as passing propsData didn't work either), so I turned to StackOverflow and asked there.

It turns out that you need to create a wrapper component that renders the functional component and forwards it all props and listeners. Then you can mount the wrapper component to be able to test the functional component.

// Cell.spec.js
import { mount } from '@vue/test-utils'
import Cell from '@/components/Cell'

const WrappedCell = {
  components: { Cell },
  template: `
    <div>
      <Cell v-bind="$attrs" v-on="$listeners" />
    </div>
  `
}

const wrapper = mount(WrappedCell, {
  propsData: {
    cellData: {
      category: 'foo',
      description: 'bar'
    }
  }
});

describe('Cell.vue', () => {
  it('should output two tds with category and description', () => {
    expect(wrapper.findAll('td')).toHaveLength(2);
    expect(wrapper.findAll('td').at(0).text()).toBe('foo');
    expect(wrapper.findAll('td').at(1).text()).toBe('bar');
  });
});

The last piece of the puzzle was figuring out how to test that a parent component that used the functional component passed it the correct props. Normally you would use wrapper.props() to test the props, but this doesn't work for a functional component as you get an error: [vue-test-utils]: wrapper.props() cannot be called on a mounted functional component.

Instead, what I figured out you can do is use a similar method as testing the functional component. When mounting a component, you can pass it a stubs option in which you can stub one component out for another. This allows us to create a wrapper component and stub it for the functional component.

Since the wrapper component isn't functional, you can call props() on it to test that we're passing it the correct data. The wrapper component doesn't need to render the functional component or even register it as a component. The only requirement is that it declares the same props as the functional component.

// ParentCell.spec.js
import { shallowMount } from '@vue/test-utils'
import ParentCell from '@/components/ParentCell'

const Cell = {
  props: ['cellData'],
  template: '<div></div>'
}

describe('ParentCell.vue', () => {

  let wrapper;
  beforeEach(() => {
    wrapper = shallowMount(ParentCell, {
      stubs: {
        Cell: Cell
      }
    });
  });

  it('should correctly render the cell', () => {
    expect(wrapper.find(Cell).exists()).toBe(true);
    expect(wrapper.find(Cell).props('cellData')).toEqual({
      category: 'foo',
      description: 'bar'
    });
  });
});