Stubbing components

You can find the test described on this page here.

Why stub?

When writing unit tests, often we want to stub parts of the code we are not interested in. A stub is simply a piece of code that stands in for another. Let's say you are writing a test for a <UserContainer> component. It looks like this:

<UserContainer>
  <UsersDisplay />
</UserContainer>

<UsersDisplay> has a created lifecycle method like this:

created() {
  axios.get("/users")
}

We want to write a test that asserts <UsersDisplay> is rendered.

axios is making an ajax request to an external service in the created hook. That means when you do mount(UserContainer), <UsersDisplay> is also mounted, and created initiates an ajax request. Since this is a unit test, we only are interested in whether <UserContainer> correctly renders <UsersDisplay> - verifying the ajax request is triggered with the correct endpoint, etc, is the responsibility of <UsersDisplay>, which should be tested in the <UsersDisplay> test file.

One way to prevent the <UsersDisplay> from initiating the ajax request is by stubbing the component out. Let's write our own components and test, to get a better understanding of the different ways and benefits of using stubs.

Creating the components

This example will use two components. The first is ParentWithAPICallChild, which simply renders another component:

<template>
  <ComponentWithAsyncCall />
</template>

<script>
import ComponentWithAsyncCall from "./ComponentWithAsyncCall.vue"

export default {
  name: "ParentWithAPICallChild",

  components: {
    ComponentWithAsyncCall
  }
}
</script>

<ParentWithAPICallChild> is a simple component. It's sole responsibility is to render <ComponentWithAsyncCall>. <ComponentWithAsyncCall>, as the name suggests, makes an ajax call using the axios http client:

<template>
  <div></div>
</template>

<script>
import axios from "axios"

export default {
  name: "ComponentWithAsyncCall",
  
  created() {
    this.makeApiCall()
  },
  
  methods: {
    async makeApiCall() {
      console.log("Making api call")
      await axios.get("https://jsonplaceholder.typicode.com/posts/1")
    }
  }
}
</script>

<ComponentWithAsyncCall> calls makeApiCall in the created lifecycle hook.

Write a test using mount

Let's start off by writing a test to verify that <ComponentWithAsyncCall> is rendered:

import { shallowMount, mount } from '@vue/test-utils'
import ParentWithAPICallChild from '@/components/ParentWithAPICallChild.vue'
import ComponentWithAsyncCall from '@/components/ComponentWithAsyncCall.vue'

describe('ParentWithAPICallChild.vue', () => {
  it('renders with mount and does initialize API call', () => {
    const wrapper = mount(ParentWithAPICallChild)

    expect(wrapper.find(ComponentWithAsyncCall).exists()).toBe(true)
  })
})

Running yarn test:unit yields:

PASS  tests/unit/ParentWithAPICallChild.spec.js

console.log src/components/ComponentWithAsyncCall.vue:17
  Making api call

The test is passing - great! However, we can do better. Notice the console.log in the test output - this comes from the makeApiCall method. Ideally we don't want to make calls to external services in our unit tests, especially when it's from a component that is not the main focus of the current test. We can use the stubs mounting option, described in the vue-test-utils docs here.

Using stubs to stub <ComponentWithAsyncCall>

Let's update the test, this time stubbing <ComponentWithAsyncCall>:

it('renders with mount and does initialize API call', () => {
  const wrapper = mount(ParentWithAPICallChild, {
    stubs: {
      ComponentWithAsyncCall: true
    }
  })

  expect(wrapper.find(ComponentWithAsyncCall).exists()).toBe(true)
})

The test still passes when yarn test:unit is run, however the console.log is nowhere to be seen. That's because passing [component]: true to stubs replaced the original component with a stub. The external interface is still the same (we can still select is using find, since the name property, which is used internally by find, is still the same). The internal methods, such as makeApiCall, are replaced by dummy methods that don't do anything - they are "stubbed out".

You can also specify the markup to use for the stub, if you like:

const wrapper = mount(ParentWithAPICallChild, {
  stubs: {
    ComponentWithAsyncCall: "<div class='stub'></div>"
  }
})

Automatically stubbing with shallowMount

Instead of using mount and manually stubbing <ComponentWithAsyncCall>, we can simply use shallowMount, which automatically stubs any other components by default. The test with shallowMount looks like this:

it('renders with shallowMount and does not initialize API call', () => {
  const wrapper = shallowMount(ParentWithAPICallChild)

  expect(wrapper.find(ComponentWithAsyncCall).exists()).toBe(true)
})

Running yarn test:unit doesn't show any console.log, and test passes. shallowMount automatically stubbed <ComponentWithAsyncCall>. shallowMount is useful for testing components that have a lot of child components, that might have behavior triggered in lifecycle hooks such as created or mounted, as so on. I tend to use shallowMount by default, unless I have a good reason to use mount. It depends on your use case, and what you are testing.

Conclusion

  • stubs is useful for stubbing out the behavior of children that is unrelated to the current unit test
  • shallowMount stubs out child components by default
  • you can pass true to create a default stub, or pass your own custom implementation

You can find the test described on this page here.

Last Updated: 2/7/2019, 7:58:45 PM