Image

unit-testing-vue-3

6 min read
Last update: December 19, 2021

Stubbing Child Components

In the previous lesson, we looked at mocking our axios module in order to test that our component was set up to make an API call and use the data that was returned, without ever having to hit our actual sever or unnecessarily coupling our test to our backend.

This concept of mocking something within our unit test is a broader topic than just mocking modules, whether they be axios or some other external dependency. In this lesson, we’ll delve deeper into this topic and look at another form of faking something within a component’s test, called stubbing, and why and when this approach might be useful.


Children with Baggage

To explore this concept, I’d like to introduce you to MessageContainer, the parent of MessageDisplay (the component we tested in the previous lesson).

📄 MessageContainer.vue

<template>
    <MessageDisplay />
</template>

<script>
    import MessageDisplay from '@/components/MessageDisplay'

    export default {
        components: {
            MessageDisplay
        }
    }
</script>

As you can see, MessageContainer simply imports and wraps MessageDisplay. This means that when MessageContainer gets rendered, MessageDisplay is also rendered. So we’re hitting up against the same problem from our previous lesson. We don’t want to actually fire the real axios get request that happens on MessageDisplay’s created hook.

📄 MessageDisplay.vue

async created() {
try {
this.message = await getMessage() // Don't want this to fire in parent test
} catch (err) {
this.error = 'Oops! Something went wrong.'
}
}

So what’s the solution here? How do we test MessageContainer without triggering its child’s axios request? Or to make the question more general: What do we do when a child component has module dependencies that we don’t want to use the real version of within our tests?

The answer to that is perhaps not a very satisfying one. Because the answer is: it depends. It depends on the complexity and number of modules that the child has. For this example, things are fairly lightweight. We only have one module, so we could simply mock axios in MessageContainer’s test, just like we did in our MessageDisplay.spec.js. But what if our child component had multiple module dependencies? In more complex cases, it’s often the simpler approach to skip mocking the child component’s module baggage and instead mock the child component itself. In other words: we can use a stub, or fake placeholder version, of the child component.

With all of that intellectual boilerplate out of the way, let’s continue with this example and see how we would stub out MessageDisplay within MessageContainer’s test.


The MessageContainer Test

We’ll start off with a scaffold:

📄 MessageContainer.spec.js

import MessageContainer from '@/components/MessageContainer'
import { mount } from '@vue/test-utils'

describe('MessageContainer', () => {
it('Wraps the MessageDisplay component', () => {
const wrapper = mount(MessageContainer)
})
})

In this test, where and how do we stub our child? Remember it’s the mounting of MessageContainer that would have created and mounted its child along with it, causing the child’s API call to fire. So it makes sense that we stub the child component when we mount its parent. To do that, we’ll add a stubs property as a second argument in our mount method.

📄 MessageContainer.spec.js

import MessageContainer from '@/components/MessageContainer'
import { mount } from '@vue/test-utils'

describe('MessageContainer', () => {
it('Wraps the MessageDisplay component', () => {
const wrapper = mount(MessageContainer, {
global: {
stubs: {
MessageDisplay: '<p data-testid="message">Hello from the db!</p>'
}
}
})
})
})

Notice how we’ve added our MessageDisplay component to the stubs property, and its value is the HTML we’d expect to be rendered if the child were to actually be mounted. Again, this stub is a placeholder that gets mounted when we mount the parent. It’s a canned response; a substitute standing in for the real child component.

Now, in order to ensure MessageContainer is doing its ****job of wrapping the MessageDisplay component, we need to look inside of what was mounted and see if the correct message from (our stubbed version of) MessageDisplay can be found.

📄 MessageContainer.spec.js

describe('MessageContainer', () => {
it('Wraps MessageDisplay component', () => {
const wrapper = mount(MessageContainer, {
global: {
stubs: {
MessageDisplay: '<p data-testid="message">Hello from the db!</p>'
}
}
})

const message = wrapper.find('[data-testid="message"]').text()
expect(message).toEqual('Hello from the db!')
})
})

We’ll create a const to store the stubMessage we are expecting to be rendered, and in our assertion we’ll compare that against the message that was mounted (from the stub).

If we run npm run test:unit in our terminal, we indeed see that our test is passing and we’ve confirmed that MessageContainer is doing its job: wrapping the (stubbed) MessageDisplay component, which displays what the real child would have. Great.


The Disadvantages of Stubbing

While stubs can help us simplify tests containing otherwise burdensome child components, we need to take a moment to consider the disadvantages of stubbing.

Since a stub is really just a placeholder for the child component, if that real component’s behavior changed, we may need to then update the stub accordingly. This can lead to maintenance costs for upkeeping our stubs as our app evolves. Generally speaking, stubs can create a coupling between the test and component’s implementation details.

Furthermore, since a stub isn’t the actual fully rendered component, you are reducing test coverage of your real component codebase, which can lead to reduced confidence that your tests are giving you truthful feedback about your app.

I bring up these points not to discourage against using stubs, but to encourage you to use them wisely and sparingly, remembering that it’s often the better practice to focus on mocking modules and service layers like we saw in the previous lesson.


What about ShallowMount?

You may have seen shallowMount being used in other people’s test code. And you may have heard that it’s a handy way for only mounting the top-level parent and not its children (thus: shallow and not deep down into child layers). So why aren’t we using that? Why are we manually stubbing out our children?

First, shallowMount falls victim to the same disadvantages (if not more) that stubs have: reduced confidence and increased coupling and maintenance. Secondly, if you start to use other libraries that sit on top of Vue Test Utils, such as Vue Testing Library, you’ll find that shallowMount isn’t supported*.* That’s why I’ve avoided teaching it. For more information on this, you can take a look at this article by Testing Library maintainer Kent C. Dodds.


Wrapping Up

As we conclude this lesson, we also conclude this course. I hope you’ve learned a lot in this introduction to unit testing your Vue apps. There are plenty more testing topics and production-level practices to cover, and you can look forward to learning more in our upcoming Unit Testing for Production course, which will be released in the coming months. Stay tuned!