Back Arrow
From the blog

How I Started Writing Unit Tests for Vue Components

In this article, we’ll take a look at how you can start testing Vue components.

Dmitry Simonov

Frontend Developer

At some point, every frontend developer realizes that it would be great to somehow verify that their code is working as expected. For me, this moment came after a painful migration from Vue 2 to Vue 3, with tons of bugs and defects. It left me with yet another strand of gray hair, and I wasn’t the only one suffering.

My project and technology stack

The current project I’m working on is a classic CRM, full of forms upon forms, list rendering, modals, and constant data transformation. The technology stack includes Vue 3, Pinia, Vite, and ElementPlus, which allows for fast and flexible interface development. 

Why unit tests?

After reading up on the theory behind pyramids and testing cups, I concluded that it’s easier to gradually add “unit” tests for new components and features, as well as cover any bugs with tests.

Just a quick note on the testing pyramid. This concept, introduced by Martin Fowler, helps organize tests in a project. It involves three levels of testing:

  • Unit Tests: the most basic level of testing, which checks individual modules or functions. Unit tests are fast, easy to set up and ensure that components work correctly in isolation. 
  • Integration Tests: test the interaction between multiple components or modules, like how components work together on a page. They are more complex and slower, but provide an understanding of the correctness of the integrations.
  • E2E tests (End-to-End Tests): test the entire application, simulating real user actions. E2E tests are slower and require complex infrastructure but ensure that the app works as a whole.

The word "unit" is in quotes here because, when testing components in a framework, there’s a nuance: testing components isn’t always about isolating them completely. 

Ideally, unit tests check isolated functions, (like the famous SUM () function), but in reality, we cannot entirely "mock" the framework. So, tests for Vue components are technically integration tests. However, for simplicity, the community continues to call them “unit tests” and I’ll stick with that terminology. 

Testing options and my choice

Of course, there were other options. For example, I considered E2E testing with Playwright, which runs tests in real browsers simulating user actions. While Playwright is a powerful tool,I faced several infrastructure difficulties, which I’ll discuss later in the article.

Spoiler: the backend wasn't ready for this, and realistically, E2E tests should be written under the guidance of QA engineers - they are experts at breaking systems. 

Ultimately, I decided to go with Vitest and Vue Test Utils as the main tools for testing. Vitest, as a test runner, was ideal, since Vite was already installed in the project, and Vue Test Utils provided all the necessary tools for mounting and modifying Vue components.

Why Vue Test Utils and not Testing Library? First, Vue Text Urils is recommended by the Vue community (Testing Library was a bit late in supporting Vue 3). Second, I follow "London School" of testing, where we mock everything around our component to keep tests as fair isolated as possible. In addition, the Testing Library is built on top of VTU, and I wanted fewer dependencies.

Installing and setting up

To get started, we need to install and configure Vitest and Vue test utils.

npm i -D vitest @vue/test-utils

Since our tests will run in a Node.js environment, we need DOM implementers. Vitest recommends either happy-dom or jsdom. 

I chose jsdom because it’s more popular and faster, though happy-dom is an option as well as you can switch quickly.

npm i jsdom -D

Configuring the testing environment 

Let's add the necessary configuration to vite.config.js.

defineConfig({
...
test: {
environment: 'jsdom',
deps: {
  inline: ['element-plus'], // optional field
},
},
})

Since Pinia is in the stack, we can either use a real store or a test store for testing. I opted for a test store, as it’s easier to mutate directly during tests.

npm i -D @pinia/testing

What and how to test?

Let's take a component with a button and a modal as an example.

Component diagram and description 

Here’s the basic flow:

  • There is a button (which is enabled if the user has permission).), 
  • When clicked, it opens a form in a modal window (a separate component).
  • When the form is filled and the "Add" button is clicked, a request is sent to the server. 
  • After a successful response, an event is emitted to indicate that the document has been added.

Component code:

<template>
 <div>
   <button :disabled="!isAddDocumentAvailable" class="button button--primary" @click="addIncomingMail">Add incoming correspondence</button>
   <popup drawer :is-form-open="isFormOpen" @closed="closeForm">
     <add-incoming-mail-modal :lawsuit-id="lawsuitId" @closeForm="closeForm" @mail-added="onMailChanged" />
   </popup>
 </div>
</template>

<script setup >
import { ref } from 'vue';
import AddIncomingMailModal from '@/.../AddIncomingMailModal.vue';
const isFormOpen = ref(false);

const emits = defineEmits(['mail-added']);
defineProps({
 lawsuitId: {
   type: Number,
   required: true,
 },	
});

function addIncomingMail() {
 isFormOpen.value = true;
}

function closeForm() {
 isFormOpen.value = false;
}
function onMailChanged() {
 emits('mail-added');
}
const isAddDocumentAvailable = computed(() => {
 return checkAvailabilityByClaim(...);
});
</script>

A bit of theory. 

To start, we need to decide what should be tested. The general advice is to follow the user's behavior and avoid focusing on implementation details, which makes sense. Essentially, we’ll treat the component as a black box: changing (preparing) its input data and testing the output.

Note: if the component has a complex function with heavy calculations, it should ideally be extracted and tested separately as a traditional unit test.

Mocks and stubs play a key role in isolating tests. Mocks replace real dependencies, while stubs simplify interaction with external systems (API, for example). The main rule is not to mock everything: only mock what affects the functionality being tested. This reduces test dependence on the mock.

Input data includes properties, components, dependency injections, external stores, and slots. For a fair test, we should replace real components, stores, and DIs with stubs. This minimizes the risk of false positives.

Output: we should test the resulting HTMLcheck  API calls (which should also be stubbed to avoid affecting the backend), and verify that events are emitted and handled correctly outside the component. 

Let's move on to writing the test. First, we need to define what we are testing (as they say, "draw a circle").

describe('Adding incoming correspondence - AddIncomingMail', () => {

 it.todo('The "Add incoming correspondence" button is active if the user has rights', async () => {});

 it.todo('By clicking on the "Add incoming correspondence" button, a modal window for adding a new document opens', async () => {});

 it.todo('When adding incoming correspondence, we generate an event outside the component', async () => {});
});

Notice how these test descriptions serve as self-documenting code — in theory.

I prefer placing tests as close to the components as possible. This approach seems better suited for understanding how the component works.

In general, we draw another circle, add details, and - we have a test.

import { getButtonByText } from '@/mocks/helpersForTesting/searchElements/index.js';
import { mount } from '@vue/test-utils';
import AddIncomingMail from '@/components/AddIncomingMail.vue';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { createTestingPinia } from '@pinia/testing';

let wrapper; // storing a component instance

beforeEach(() => { // before each test we mount the component with the initial settings
 wrapper = mount(AddIncomingMail, {
   global: {
	plugins:[    
createTestingPinia({ // Create a test instance of Pinia
         createSpy: vi.fn,
         stubActions: false,
         initialState: {
           user: {
             state: {
               claims: [1,2],
             },
           },
         },
       }),]
     stubs: { // Since this is unit testing, we replace all child components with stubs
       AddIncomingMailModal: {
         name: 'AddIncomingMailModal',
         emits: ['mail-added'],
         template: '<div><h2 >Add correspondence</h2></div>',
       },
       popup: {
         props: { isFormOpen: false },
         template: '<div v-if="isFormOpen"><slot/></div>',
       },
     },
   },
   props: {
     lawsuitId: 15,
   },
 });
});

afterEach(() => { // After each test, we reset the stubs and destroy the instance
 wrapper.unmount();
 vi.resetAllMocks();
});

describe('Adding incoming correspondence - AddIncomingMail', () => {
 it('The "Add incoming correspondence" button is active if the user has rights', async () => {
   const button = getButtonByText({ wrapper, buttonText: 'Add incoming correspondence' }); // Made a helper for quick search of buttons

expect(button.element.disabled).toBe(false);
});

 it('By clicking on the "Add incoming correspondence" button, a modal window for adding a new document opens', async () => {
   const button = getButtonByText({ wrapper, buttonText: 'Add incoming correspondence' });

   await button.trigger('click');
   await wrapper.vm.$nextTick(); //Since Vue makes changes asynchronously, it is important to wait

   expect(wrapper.text()).contain('Add correspondence');
 });

it('When adding incoming correspondence, we generate an event outside the component', async () => {
   const button = getButtonByText({ wrapper, buttonText: 'Add incoming correspondence' });
   await button.trigger('click');
   await wrapper.vm.$nextTick();
   const modalStub = wrapper.findComponent({ name: 'AddIncomingMailModal' });

   modalStub.vm.$emit('mail-added');
   expect(wrapper.emitted()).toHaveProperty('mail-added');
 });
});

I would like to draw your attention to how element searching occurs. You should try to search for elements the way a user would search. A less recommended, but more popular approach is using data-testid=”foo”.

Note: settings like the ones below:
afterEach(() => { // After each test, we reset the stubs and destroy the instance
  wrapper.unmount();
  vi.resetAllMocks();
});

can be moved to external files and connected in the vite.config.js settings as a global setting:
// vite.config.js
export default defineConfig({
  test: {
    // ...
    setupFiles: ['path to your file'],
    // ...
  },
});

Problem with unit tests

Ultimately, unit tests help establish a contract between components. However, there's a downside: if we change the modal’s code, for example, its emitted events, the test for AddIncomingMail won’t fail. 

This is where integration tests (as mentioned earlier) come in, testing larger sections of the app. 

I haven’t fully implemented those tests yet, as I’m still thinking about the infrastructure and mocks (particularly for server requests), through a conditional msw. However, there is a problem with maintaining the relevance of mocked responses (fixtures). Ideally, I'd like to use “Vitest browser mode” or “Playwright CT”, but these tools are still in their infancy. 

Conclusion

Start testing your code early and don’t treat testing as something separate from development. Yes, at first it will be tough, and yes, it will continue to be tough, but soon enough, you’ll just get used to it.

It's easy to start working with us. Just fill the brief or call us.

Find out more
White Arrow
From the blog
Related articles

Inspecting raw database data in Xperience by Kentico

Dmitry Bastron

This article is a cheat sheet to inspect what's going on with the imported data by Xperience by Kentico Migration Toolkit, resolve some CI/CD issues and on many other occasions!

Kentico

Learnings from using Sitecore ADM

Anna Bastron

Let's try to understand how the ADM module works, its limitations and tactics for optimising its performance.

Sitecore

Your last migration to Xperience by Kentico

Dmitry Bastron

The more mature Xperience by Kentico product becomes, the more often I hear "How can we migrate there?”

Kentico

5 Key Software Architecture Principles for Starting Your Next Project

Andrey Stepanov

In this article, we will touch on where to start designing the architecture and how to make sure that you don’t have to redo it during the process.

Architecture
Software development

Assessing Algorithm Complexity in C#: Memory and Time Examples

Anton Vorotyncev

Today, we will talk about assessing algorithm complexity and clearly demonstrate how this complexity affects the performance of the code.

.NET

Top 8 B2B Client Service Trends to Watch in 2024

Tatiana Golovacheva

The development market today feels like a race - each lap is quicker, and one wrong move can cost you. In this race, excellent client service can either add extra points or lead to a loss dot to high competition.

Customer Service
Client Service

8 Non-Obvious Vulnerabilities in E-Commerce Projects Built with NextJS

Dmitry Bastron

Ensuring security during development is crucial, especially as online and e-commerce services become more complex. To mitigate risks, we train developers in web security basics and regularly perform third-party penetration testing before launch.

Next.js
Development

How personalisation works in Sitecore XM Cloud

Anna Bastron

In my previous article, I shared a comprehensive troubleshooting guide for Sitecore XM Cloud tracking and personalisation. This article visualises what happens behind the scenes when you enable personalisation and tracking in your Sitecore XM Cloud applications.

Sitecore

Server and client components in Next.js: when, how and why?

Sergei Pestov

All the text and examples in this article refer to Next.js 13.4 and newer versions, in which React Server Components have gained stable status and became the recommended approach for developing applications using Next.js.

Next.js

How to properly measure code speed in .NET

Anton Vorotyncev

Imagine you have a solution to a problem or a task, and now you need to evaluate the optimality of this solution from a performance perspective.

.NET

Formalizing API Workflow in .NET Microservices

Artyom Chernenko

Let's talk about how to organize the interaction of microservices in a large, long-lived product, both synchronously and asynchronously.

.NET

Hidden Aspects of TypeScript and How to Resolve Them

Dmitry Berdnikov

We suggest using a special editor to immediately check each example while reading the article. This editor is convenient because you can switch the TypeScript version in it.

TypeScript

Troubleshooting tracking and personalisation in Sitecore XM Cloud

Anna Gevel

One of the first things I tested in Sitecore XM Cloud was embedded tracking and personalisation capabilities. It has been really interesting to see what is available out-of-the-box, how much flexibility XM Cloud offers to marketing teams and what is required from developers to set it up.

Sitecore

Mastering advanced tracking with Kentico Xperience

Dmitry Bastron

We will take you on a journey through a real-life scenario of implementing advanced tracking and analytics using Kentico Xperience 13 DXP.

Kentico
Devtools

Why is Kentico of such significance to us?

Anastasia Medvedeva

Kentico stands as one of our principal development tools, we believe it would be fitting to address why we opt to work with Kentico and why we allocate substantial time to cultivating our experts in this DXP.

Kentico

Where to start learning Sitecore - An interview with Sitecore MVP Anna Gevel

Anna Gevel

As a software development company, we at Byteminds truly believe that learning and sharing knowledge is one of the best ways of growing technical expertise.

Sitecore

Sitecore replatforming and upgrades

Anastasia Medvedeva

Our expertise spans full-scale builds and support to upgrades and replatforming.

Sitecore

How we improved page load speed for Next.js ecommerce website by 50%

Sergei Pestov

How to stop declining of the performance indicators of your ecommerce website and perform optimising page load performance.

Next.js

Sitecore integration with Azure Active Directory B2C

Dmitry Bastron

We would like to share our experience of integrating Sitecore 9.3 with the Azure AD B2C (Azure Active Directory Business to Consumer) user management system.

Sitecore
Azure

Dynamic URL routing with Kontent.ai

We'll consider the top-to-bottom approach for modeling content relationships, as it is more user-friendly for content editors working in the Kontent.ai admin interface.

Kontent Ai

Headless CMS. Identifying Ideal Use Cases and Speeding Up Time-to-Market

Andrey Stepanov

All you need to know about Headless CMS. We also share the knowledge about benefits of Headless CMS, its pros and cons.

Headless CMS

Enterprise projects: what does a developer need to know?

Fedor Kiselev

Let's talk about what enterprise development is, what nuance enterprise projects may have, and which skills you need to acquire to successfully work within the .NET stack.

Development

Fixed Price, Time & Materials, and Retainer: How to Choose the Right Agreement for Your Project with Us

Andrey Stepanov

We will explain how these agreements differ from one another and what projects they are suitable for.

Customer success

Sitecore Personalize: tips & tricks for decision models and programmable nodes

Anna Gevel

We've collected various findings around decision models and programmable nodes working with Sitecore Personalize.

Sitecore

Umbraco replatforming and upgrades

Anastasia Medvedeva

Our team boasts several developers experienced in working with Umbraco, specialising in development, upgrading, and replatforming from other CMS to Umbraco.

Umbraco

Kentico replatforming and upgrades

Anastasia Medvedeva

Since 2015, we've been harnessing Kentico's capabilities well beyond its core CMS functions.

Kentico

Interesting features of devtools for QA

Egor Yaroslavcev

Chrome DevTools serves as a developer console, offering an array of in-browser tools for constructing and debugging websites and applications.

Devtools
QA

Activity logging with Xperience by Kentico

Dmitry Bastron

We'll dive into practical implementation in your Xperience by Kentico project. We'll guide you through setting up a custom activity type and show you how to log visitor activities effectively.

Kentico
This website uses cookies. View Privacy Policy.