Skip to content
Muhammet Şafak
tr
Tools & Technologies 4 min read

Testing Vue Components

A practical approach to automatically verifying Vue component UI behavior: writing real-world tests with Vue Test Utils.


I had already developed a solid habit of writing tests with PHPUnit on the PHP side. But when it came to frontend components, I kept putting testing off. I told myself “testing UIs is hard” — which is partly true, but laziness played a role in that delay. Now that I’m using Vue Test Utils properly, I can see that testing Vue components is far more practical than I ever imagined.

In this post I’m going to skip the “why should you write tests?” question and jump straight into “how do you do it?”

The toolset

The standard test combination for Vue 2: Vue Test Utils + Jest. Vue Test Utils is the official helper library that lets you render components, interact with them, and query their output. Jest is the test runner and assertion library.

Installation:

npm install --save-dev @vue/test-utils jest vue-jest babel-jest

You need to transform Vue files with vue-jest inside jest.config.js.

Your first component test

Let’s take a simple Counter.vue component:

<template>
  <div>
    <span data-testid="count">{{ count }}</span>
    <button @click="increment">Artır</button>
  </div>
</template>

<script>
export default {
  data() {
    return { count: 0 };
  },
  methods: {
    increment() {
      this.count++;
    }
  }
};
</script>

And its test:

import { shallowMount } from '@vue/test-utils';
import Counter from '@/components/Counter.vue';

describe('Counter', () => {
  it('başlangıçta 0 gösterir', () => {
    const wrapper = shallowMount(Counter);
    expect(wrapper.find('[data-testid="count"]').text()).toBe('0');
  });

  it('butona tıklayınca değer artar', async () => {
    const wrapper = shallowMount(Counter);
    await wrapper.find('button').trigger('click');
    expect(wrapper.find('[data-testid="count"]').text()).toBe('1');
  });
});

shallowMount renders the component shallowly: child components are replaced with stubs. mount renders the full tree. For isolation purposes, shallowMount is usually preferable.

Using data-testid attributes has become a habit of mine. CSS classes and element selectors break tests when they change; data-testid exists solely for testing and stays stable.

Testing props and emits

Testing how a component behaves based on its props, and which events it emits:

import { shallowMount } from '@vue/test-utils';
import ConfirmDialog from '@/components/ConfirmDialog.vue';

describe('ConfirmDialog', () => {
  it('message prop\'unu gösterir', () => {
    const wrapper = shallowMount(ConfirmDialog, {
      propsData: { message: 'Silmek istediğinizden emin misiniz?' }
    });
    expect(wrapper.text()).toContain('Silmek istediğinizden emin misiniz?');
  });

  it('onayla butonuna tıklayınca confirm eventi yayar', async () => {
    const wrapper = shallowMount(ConfirmDialog, {
      propsData: { message: 'Test' }
    });
    await wrapper.find('[data-testid="confirm-btn"]').trigger('click');
    expect(wrapper.emitted('confirm')).toBeTruthy();
  });
});

Emit tests feel a bit strange coming from the backend, because you’re not testing the receiver of the event — you’re simply checking “was it emitted?” But this is the right approach: the component’s responsibility is to emit, and the parent component decides what to do with it. Keep the scope tight and the test stays clear.

Testing components with a Vuex store

Components connected to a store require a bit more setup. There are two approaches: creating a real store or mocking the store. For isolated unit tests, I prefer mocking:

import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import UserList from '@/components/UserList.vue';

const localVue = createLocalVue();
localVue.use(Vuex);

describe('UserList', () => {
  let store;

  beforeEach(() => {
    store = new Vuex.Store({
      state: {
        users: [
          { id: 1, name: 'Ali' },
          { id: 2, name: 'Ayşe' }
        ]
      }
    });
  });

  it('store\'daki kullanıcıları listeler', () => {
    const wrapper = shallowMount(UserList, { store, localVue });
    expect(wrapper.findAll('[data-testid="user-item"]')).toHaveLength(2);
  });
});

One thing I’m careful about here: recreating the store inside beforeEach for every test. A shared store can let one test contaminate another. Just like starting with a clean database in PHPUnit, every test here starts with a clean store.

What to test, and what not to?

You don’t need to test every line. Valuable tests are:

  • How the component responds to user input.
  • How rendering changes based on props.
  • Critical conditional branches (conditional rendering).
  • Emitted events.

Testing CSS styles or animations is generally not worth the cost. Test the component’s behavior, not its appearance.

On the backend you test “does this endpoint return 200 or 403?”; on the frontend you test “when I click this button, does the right thing happen?” The frame is different, the logic is the same.

Writing frontend tests should become as ingrained a habit as writing backend tests. Clicking through a component manually and calling it “working” is not enough — especially when a component depends on another and you’re about to make changes. The first time I refactored with tests in place and watched that safety net catch things, I was genuinely surprised it had taken me so long.

There’s another thing I noticed: when you write a test, you’re looking at the component from the outside — what are its props, what events does it emit, what does the user see? That perspective forces you to think about how the component will be used from the outside. Well-written test scenarios point toward a well-designed component interface. The clarity that testing demands also improves the design. If a component becomes hard to test, that’s almost always a sign that it’s taken on too many responsibilities; making the test easier by breaking down the component improves both the test scenario and the actual code.

Share:

Comments

Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.

Related Posts

Search the site

Start typing to search posts, projects and pages.

Esc to close Powered by Pagefind