Testing Concept π
Testing is a methodology applied for checking if the code written is actually giving the desired output.
It is a must to test your components for the following reasons:
- Minimize regressions
- Ensure code integrity, scalability, and quality
- Monitor performance
- Get a safe development setup
Unit testing π
Unit testing is basically focusing on the outputs in a component scale, since Vue is actually based on the component design system.
Before we go deeper, we need to know and understand what to test actually and how to structure our tests accordingly.
What to test β
Many of my colleagues are actually testing component inputs ββ. This actually is not what the testing concept is here for, therefore, We need actually to test component output instead. We'll be using @vue/test-utils
with jest
testing framework.
Testing component output
To organize this a bit, here are the things we need actually to
test in a Vue component:
- Rendered template
- Emitted events
- Side effects (VueX actions, vue-router, calling imported functions, methods, mixins, .... etc)
Now I will show the traditional way β (incorrect) β that most developers do to structure their tests:
describe('methods', () => {
/* Testing every method in isolation */
})
describe('computed', () => {
/* Testing every computed property in isolation */
})
describe('template', () => {
/* Testing what is rendered. With the snapshot */
})
As shown above , the tests look structured. However, it is following the context of testing the β inputs β instead of the βοΈ outputs βοΈ!
let's have a look at this simple template:
<template>
<main>
<div v-if="loading">
Loading ...
</div>
<template v-else>
<p v-if="error">
Something went wrong!
</p>
<div v-else>
<!-- some data -->
</div>
</template>
</main>
</template>
As seen above , it is a simple component that's setup for synchronous fetching of some data from the API. To test this out, let's think about it as a state machine.
So the component either gets data, or loads an error, right?
Now let's look at this testing structure:
describe('when loading', () => {
it.todo(`renders 'Loading...' text`)
it.todo(`does not render the error message`)
it.todo(`does not render data`)
})
describe('when there is an error', () => {
it.todo(`does not render 'Loading...' text`)
it.todo(`renders error message`)
it.todo(`does not render data`)
})
So, in the example above, we have divided the test specs into 2
main groups as we have 2
main phases we should test:
- Within loading
- When there is an error
This will organize our specs a bit, as our component might not render the error message while loading if something happened for some reason, or it might be actually in loading state, but it is not rendering the loading text.
That way, our testing spec will be more logical and this makes it easier to interpret and debug without any headache.
Start with component factory
Component factory is simply a method that creates (shallow mounts) Vue component
import { shallowMount } from '@vue/test-utils';
describe('My component test', () => {
let wrapper;
// Component Factory
function createComponent() {
wrapper = shallowMount(MyComponent, {/* optional params */})
}
// Destroy wrapper
afterEach(() => {
wrapper.destroy()
})
})
The previous snippet shows that we create a changing wrapper
variable and we do optionally set a createComponent()
function, but why is that?
The thing is, in some test cases, you might try to mount the component with different props, or may be add some mocks. So we will need to change the wrapper and remount the component.
Use helpers to help you find elements and components
For very complex components, we may use helpers to help us find elements and components easily.
Let's take a look at this snippet:
import { shallowMount } from '@vue/test-utils';
describe('My component test', () => {
let wrapper;
const findConfirmBtn = wrapper.find('[data-testid="confirm-btn"]')
const findModalComp = wrapper.findComponent(MyModalComponent)
// Component Factory
function createComponent() {
wrapper = shallowMount(MyComponent, {/* optional params */})
}
// Destroy wrapper
afterEach(() => {
wrapper.destroy()
})
it('renders a modal', () => {
createComponent();
expect(findModalComp.exists()).toBe(true)
})
})
So, as we saw there, we have created like a boiler plate for locating different elements and we did make use of the createComponent()
function which is really great!
Note => using data selectors like
[data-testid="something"]
is a best practice
the [data-testid="something"]
is important because we do apply refactors from time to time and we might change either the component name or the classes attached to that component. This will guarantee that the test specs won't be affected and we are good to go.
β Never β ever β test component internals
It is a really bad practice to test the component internals. Let me show you an example:
export default {
data() {
return {
count: 0
}
}
computed: {
double() {
return this.count * 2
}
}
methods: {
incrementCount() {
this.count++
}
}
}
The normal way that comes in mind to test this out will be something like this:
it('Calculates double correctly', () => {
createComponent({ data: { count: 1 } })
expect(wrapper.vm.double).toBe(2)
})
it('Calls correct method on btn click', () => {
createComponent()
jest.spyOn(wrapper.vm, 'incrementCount').mockImplementation(() => {})
findIncrementBtn().trigger('click')
expect(wrapper.vm.incrementCount).toHaveBeenCalled()
expect(wrapper.vm.count).toBe(1)
})
This is actually a wrong approach βββ as it tests if the method is called when clicking the btn. That way, we are retesting Vue framework and thus, this is way far from testing our logic.
if you are saying: "hey! We should test our computed props in isolation π΅", then you are trying to reverse the engineering made in the creation of the SFC component. It is not practical to isolate a computed prop from the SFC as this will affect rendering the template!
In this case we can say that, the best way to check the computed props is with rendering the template βοΈ. I will show you how in a moment.
So, let's imagine that our template is looking like this:
<template>
<div>
<span data-testid="count">Count is: {{ count }}</div>
<button data-testid="increment-button" @click="incrementCount">
Inctrement
</button>
<p data-testid="double">Count x2: {{ double }}</p>
</div>
</template>
So, instead of testing the internal options API props. We can test the rendered results/outputs in the template itself βοΈ like so:
const findDouble = wrapper.find('[data-testid="double"]')
it('Calculates double correctly', () => {
createComponent({ data: { count: 1 } })
// expect(wrapper.vm.double).toBe(2) //This was the wrong approach
expect(findDouble().text()).toBe(`Count x2: 2`) // This is the best practice
})
// for an extended version, jest supports this format
it.each`
a | expected
${0} | ${0}
${1} | ${2}
${10} | ${20}
${100} | ${200}
`('renders double count as $expected when count is $a',
({ a, expected } => {
createComponent({ data: { count: a } })
expect(findDouble().text()).toBe(`Count x2: ${expected}`)
})
)
This way, we are neither checking the template nor checking the internal props because we don't have to. Instead, we are checking the outputs in the template βοΈ βοΈ βοΈ.
That means, we don't care about how the logic was built for doubling a count as long as the output is always correct. that's why we do test edge cases to make sure that there are no regressions what so ever.
With the same approach we can test the rest of data
and methods
the same way like this:
const findCount = () => wrapper.find('[data-testid="count"]')
const findIncrementBtn = () => wrapper.find('[data-testid="increment-btn"]')
it('Calls correct method on btn click', async () => {
createComponent()
expect(findCount().text()).toBe('Count: 0')
findIncrementBtn().trigger('click')
await nextTick()
expect(findCount().text()).toBe('Count: 1')
})
This way βοΈ we are testing the rendered output on the template.
π Rules of thumb π
- Forget about asserting
wrapper.vm
- Never spy on methods
- If we rename method or computed, test should pass because we do care about the output only
Why we shouldn't test the component internals β
The trick here is that when you test a method in isolation, it passes, but if a developer references it wrongly in the template, the test will still pass and that is not what we are targeting, as the custom component will be still wrong and we are testing Vue itself π
We should test the rendered output to manage the typos, bugs or wrong references. So, the test should not pass if we reference the wrong attributes or methods in the template.
Always follow the user
Back to our example
it('Calculates double correctly', () => {
createComponent({ data: { count: 1 } })
expect(findDouble().text()).toBe(`Count x2: 2`)
// now if the user increases the count
wrapper.setData({ count: 2})
expect(findDouble().text()).toBe(`Count x2: 4`)
})
This test looks okay, but still wrong βββ.. as we should be testing the user interaction itself
it('Calculates double correctly', async() => {
createComponent({ data: { count: 1 } })
expect(findDouble().text()).toBe(`Count x2: 2`)
// now if the user increases the count
findIncrementBtn().trigger('click')
await nextTick()
expect(findDouble().text()).toBe(`Count x2: 4`)
})
This way, we are checking when user clicks a button βοΈ, it should reflect the value change in the template, and that way, our test is touching the business logic that we need actually to verify βοΈ.
Final thoughts
Child components are black boxes
We should be using shallowMount
instead of mount as we need to focus on the component we are testing.
Don't forget about Vue microtasks
Be sure not to use microtasks like nextTick
, otherwise, the test expectation will fail.
Happy coding! π»
Top comments (0)