I have to be honest with you: on my previous experiences working with React, I've procrastinated a lot on deep dive at component unit testing. Unlike other concepts (like states, props, life cycles), test is a knowledge which you wont learn only by doing over and over again: know the fundamentals of testing is essential to know what to test.
Recently, I've started to work on an application using Vue.js and despite of my lack of knowledge on component testing, I knew something was wrong on its tests. I know that because software testing is something that i’m studying a lot in the previous months and I knew the difference of concepts like behaviour vs implementation tests to evaluate the quality of current tests.
PS: If you want to know more about the overall concepts of tests, check out my recent article about Glenford Myers books, The Art of Software Testing.
Right now, working on an application with a need to component test improving, I had to study some of the fundamentals of component unit testing to start a new history on this company. This article is intend to provide an overview and give some references of how you can go further on your own journey on component unit testing. The main references of this articles are:
- Vue Mastery: Real World Testing, with Jessica Sachs
- Testing Vue.js Applications, from Edd Yerburgh
- Vue Testing Handbook, from Lachlan Miller
- Kent C. Dodds’s Blog
Purpose of Test
When it comes to test, starts on its philosophies is a perfect start. Just to give an example of the power of tests philosophy, one that really changed my mind, on Glenford books, was a shift from thinking in test as a process to show that a program works correctly to a greater definition:
Testing is the process of executing a program with the intent of finding errors.
Saying that, on components world, our main philosophy on test is to ensure that the component holds its contracts for the end user. From the Testing Vue.js Applications’ book:
Deciding what unit tests to write is important. If you wrote tests for every property of a component, you would slow development and create an inefficient test suite. One method for deciding what parts of a component should be tested is to use the concept of a component contract. A component contract is the agreement between a component and the rest of the application.
...
The idea of input and output is important in component contracts. A good component unit test should always trigger an input and assert that the component generates the correct output. You should write tests from the perspective of a developer who’s using the component but is unaware of how the component functionality is implemented.
What is NOT the purpose of test
- The fallacy of 100% code coverage: Sometimes the test coverage is the only metric that we have to ensure if our test suite are in state of excellence. But we have to be careful because this metric can be misleading. Things like the component contract mentioned above and a user perspective (what are the most crucial parts of your app) are far more effective than be based only on test coverage. Another idea, explained in this article of Kent C. Odds, is that for test coverage an “About” page and a “Checkout” one would be the same thing, but for our end users one is far more important than the other one. This is explained as well by Edd, on Part 1.2.7 of his book:
Not only is it time-consuming to reach the fabled 100% code coverage, but even with 100% code coverage, tests do not always catch bugs. Sometimes you make the wrong assumptions. Maybe you’re testing code that calls an API, and you assume that the API never returns an error; when the API does return an error in production, your app will implode.
You don’t become a test master by striving for 100% code coverage on every app. Like a good MMA fighter who knows when to walk away from a fight, a true testing master knows when to write tests and when not to write tests.
- Implementation details: Things like our implementation process (variable names, hard coded label names, etc) are just implementation details, it does not necessary aims to ensure the component contract. Take a look of a test that I've found on my recent Vue.js experience
describe("CarForm", () => {
it("contains correct form labels", async () => {
const { queryByText } = renderComponent();
expect(queryByText("Brand")).toBeTruthy();
expect(queryByText("Price")).toBeTruthy();
expect(queryByText("Year")).toBeTruthy();
expect(queryByText("Submit")).toBeTruthy();
expect(queryByText("Cancel")).toBeTruthy();
});
}
This test aims to test form labels; its all implementation details, these names can change easily. When comes to form, things like state changes on submit, submit function being called correctly and submit experience (successful, error) are contracts from forms that are far more important, like this example from Vue Testing Handbook
- the framework itself or third party libraries: We do not have to test methods and function from the framework itself neither from third party libraries. You have to trust that the core team from these tools have already done this job.
How to test
The more your tests resemble the way of your software is used, the more confidence they can give you — Kent C. Dodds
On writing component tests, you have to think gradually to test. Use the follow mentality:
1) Create a test suite (describe (…) ) and setup your tests (test(…))
This part is the easiest one: create a describe and a test to ensure your test setup is working correctly. This could be unnecessary but if you do not do that and, for any reason, and there is a problem in the setup, it will make the debug process harder.
2) Mount the component
According to Jessica Sachs, this is the hardest part on writing component unit tests. In this step, your only concern is to make sure your component will be mounted without any error. The difficult can vary from a lot of things, like:
- Plug dependencies correctly (like redux, pinia, router, etc) on component;
- Set props correctly;
- Build mocks correctly;
To go further on this step, read this section called Mounting Options from Vue Test Utils. I’ve tried to find the equivalence for React, but I did not. But this article, talking about Enzyme gave me a good reference.
3) Leave your IDE and think about component contracts
As mentioned above, test is not a part of the software that you simple learn by doing, you have to think about what is going to be tested. At this point of this article, I hope you already built a mentality that your unit tests should:
- guarantee that your component contract is going to be satisfied, independent of its implementations
- End-user mindset to know the most fundamental contracts️.
Saying that, in this part you will think and write your component contracts — which will be each test case. This will make the processes to build tests easier. An example of this could be checked on Lesson 3, from Vue Mastery Unit Test Course. This lesson is only available for premium accounts but the repo from this lesson is available for free here.
For a RandomNumber
component:
<template>
<div>
<span>{{ randomNumber }}</span>
<button @click="getRandomNumber">Generate Random Number</button>
</div>
</template>
<script>
export default {
props: {
min: {
type: Number,
default: 1
},
max: {
type: Number,
default: 10
}
},
data() {
return {
randomNumber: 0
}
},
methods: {
getRandomNumber() {
this.randomNumber =
Math.floor(Math.random() * (this.max - this.min + 1)) + this.min
}
}
}
</script>
The author draw three contracts:
import RandomNumber from '@/components/RandomNumber'
import { mount } from '@vue/test-utils'
describe('RandomNumber', () => {
test('By default, randomNumber data value should be 0', () => {
})
test('If button is clicked, randomNumber should be between 1 and 10', () => {
})
test('If button is clicked (with right props), randomNumber should be between 200 and 300', () => {
})
})
4) Write your tests
When all the previous parts of this guide are done, write tests should be easy. Here are some final tips and a work material to go further:
Use
data-testid
to grab selectors: This selectors is a good option because, as a tester, you know for sure that this won’t change; different from classes, ids, names and etc. Read this article to know more.Think of all the relevant inputs from a end user: This means that you want to check component props, actions (click, mouse over, etc), changing on states and etc. Read this section from Vue Testing Handbook to know more.
Test cases when user do the wrong stuff: You should write tests for situations where your uses calls a form with wrong data, badly formatted data or any other user errors (this is called fault injection). Remember: Testing is the process of executing a program with the intent of finding errors, do not think your user is always do the right thing. Read this article to know more.
Specialists section
From Vue.js repository
This test ensures the agreement that the computed property will fires reactivity and update the value
packages > reactivity > __tests__ > computed.spec.ts
describe('reactivity/computed', () => {
it('should return updated value', () => {
const value = reactive<{ foo?: number }>({})
const cValue = computed(() => value.foo)
expect(cValue.value).toBe(undefined)
value.foo = 1
expect(cValue.value).toBe(1)
})
From React.js repository
This test even add comments about the agreements from react states
packages > react-refresh > src > __tests__ > ReactFresh-test.js
it('can preserve state for compatible types', () => {
if (__DEV__) {
const HelloV1 = render(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello, 'Hello');
return Hello;
});
// Bump the state before patching.
const el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el.textContent).toBe('1');
// Perform a hot update.
const HelloV2 = patch(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello, 'Hello');
return Hello;
});
Top comments (0)